diff options
| author | sarthurdev <965089+sarthurdev@users.noreply.github.com> | 2025-09-14 20:16:36 +0200 |
|---|---|---|
| committer | sarthurdev <965089+sarthurdev@users.noreply.github.com> | 2025-09-15 19:07:42 +0200 |
| commit | 267fb0821829b03d33284fa22f3eb31b17f409ff (patch) | |
| tree | 9b4feed036cd7ce2ec504969da8ed8a042fe1e1a | |
| parent | 1a6747b038ea49c78b801362ffbabf18eb09543a (diff) | |
| download | vyos-build-267fb0821829b03d33284fa22f3eb31b17f409ff.tar.gz vyos-build-267fb0821829b03d33284fa22f3eb31b17f409ff.zip | |
kea: T7281: Update Kea to 3.0
9 files changed, 32 insertions, 13427 deletions
diff --git a/.github/workflows/trigger_rebuild_packages.yml b/.github/workflows/trigger_rebuild_packages.yml index 4eb740f3..e74a09b1 100644 --- a/.github/workflows/trigger_rebuild_packages.yml +++ b/.github/workflows/trigger_rebuild_packages.yml @@ -49,8 +49,6 @@ jobs: - 'scripts/package-build/hsflowd/**' isc-dhcp: - 'scripts/package-build/isc-dhcp/**' - kea: - - 'scripts/package-build/kea/**' keepalived: - 'scripts/package-build/keepalived/**' libnss-mapuser: @@ -167,10 +165,6 @@ jobs: trigger_build "isc-dhcp" fi - if [ "${{ steps.changes.outputs.kea }}" == "true" ]; then - trigger_build "kea" - fi - if [ "${{ steps.changes.outputs.keepalived }}" == "true" ]; then trigger_build "keepalived" fi diff --git a/data/architectures/amd64.toml b/data/architectures/amd64.toml index 8676ad2f..2e83358f 100644 --- a/data/architectures/amd64.toml +++ b/data/architectures/amd64.toml @@ -19,3 +19,8 @@ packages = [ [additional_repositories.zabbix] url = "https://repo.zabbix.com/zabbix/6.0/debian" + +[additional_repositories.kea] + architecture = "amd64" + url = "https://dl.cloudsmith.io/public/isc/kea-3-0/deb/debian" + distribution = "bookworm" diff --git a/data/live-build-config/archives/isc-kea.key.chroot b/data/live-build-config/archives/isc-kea.key.chroot new file mode 100644 index 00000000..0277c929 --- /dev/null +++ b/data/live-build-config/archives/isc-kea.key.chroot @@ -0,0 +1,24 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBGhVJw8BDACXwlMdVKg40L87xcyG6fuo1KAOdk3cqx0jcojtYXYo16R5A1xD +ILW/Nw/b9qWkoXAjCOL8xgspSYx8Cqg73o+Jy2tzBNSzXGYmh/8sDicJmTGBt9n7 +AhwlhW7ztzYYNaJeyyGvWR+JMqGtPWuc/LixtCu/SzA6siMKCTZfUp/3nqiWQiYV +zyYCuUI9MTaS0/LCfEclORE2GU5cED4LJli5iZFcJ8RXXTfaCncIeYFFvJTPf9TP +mbKS8QGPYMPdtwsvqBEpej3pDXHetWuIIchPokq/rSa3QX7NmBhvh14ptaIu0usn +i+5XvExH4a5RC0qgHFdLeYskzlRqw3dLwGHYZZ8geLaSrLSdla2rjQFzZhEcIDKI +FnGaoAj0B3WtpJPvhmWW/1Ecryq0iOgtdNXdYxnun8eCEW6Bo5ebAW7NAI9kmWto +i+YFsVUYAZF+gP856BSaiDMFi7LV4bOu9p78o0s6DGmBe+yVPW1Lo7Vi0X8dcLnZ +07Cy0JoN+QRlqgMAEQEAAbQ4Q2xvdWRzbWl0aCBQYWNrYWdlIChpc2Mva2VhLTMt +MCkgPHN1cHBvcnRAY2xvdWRzbWl0aC5pbz6JAc4EEwEIADgWIQSdpXC7GSIRiF5O +soCxbETNRVFMPAUCaFUnDwIbLwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRCx +bETNRVFMPPxAC/0eSSK+2Z4AOsyGun2zBDLPp3qEFrAl5KjAeZCgtYJs3UiQ65DX +0iuvdq39SRWkSRw5w5og+nZbPdDzhw3KFSVgdCsQBv83DUH0Zlmn5Wf6eTh72T5F +lmaaE02picOD3/9L0RdxZOEns/19f2OtSzRHxRMKuv7crcAsnJlkWYP2w96AM0ln +QqmpVDbI8XWtQclJUkPPU+I/m6nG7Z+tc8miJ1uBfA0wP22Zj6HzRTWf9patcfTy +IDr9rO5KmiAlO+f3YbVsKGNcuxdbMusDwpe13BRfKFDlwLKbdrSeSIw1JQc2uhhX +/u1QTAXfLSoP4VAYwbfotgG9a/LKRJGG/M9mMtroOQYO15y+Vo2uSN4q5krv589h +l6MAPFr2sDiedibMl9SGAPtT83afCrSZ6BgobytsdtV3u2WOqxrVUXDpvhZKjxX6 +pXrrSV9tPhZlq5pZvnDCS392FlMfHmjR4BUxzwD4Wnax8G7uQfEUWMkTaqQAZfuN +EsbY2TeLjn3ZojI= +=+igW +-----END PGP PUBLIC KEY BLOCK----- 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 04ca7f3e..7e20f02a 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 @@ -5,9 +5,9 @@ systemctl disable syslog.service systemctl disable rsyslog.service systemctl disable arpwatch.service systemctl disable smartd.service -systemctl disable kea-ctrl-agent.service -systemctl disable kea-dhcp4-server.service -systemctl disable kea-dhcp6-server.service +systemctl disable kea-dhcp4.service +systemctl disable kea-dhcp6.service +systemctl disable kea-dhcp-ddns.service systemctl disable isc-dhcp-relay.service systemctl disable nfacctd.service systemctl disable pmacctd.service diff --git a/scripts/package-build/kea/.gitignore b/scripts/package-build/kea/.gitignore deleted file mode 100644 index 70219f63..00000000 --- a/scripts/package-build/kea/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/isc-kea/ diff --git a/scripts/package-build/kea/build.py b/scripts/package-build/kea/build.py deleted file mode 120000 index 3c76af73..00000000 --- a/scripts/package-build/kea/build.py +++ /dev/null @@ -1 +0,0 @@ -../build.py
\ No newline at end of file diff --git a/scripts/package-build/kea/package.toml b/scripts/package-build/kea/package.toml deleted file mode 100644 index 872be441..00000000 --- a/scripts/package-build/kea/package.toml +++ /dev/null @@ -1,4 +0,0 @@ -[[packages]] -name = "isc-kea" -commit_id = "debian/2.6.1-2" -scm_url = "https://salsa.debian.org/debian/isc-kea" diff --git a/scripts/package-build/kea/patches/isc-kea/0001-Add-multithreading-test-mode.patch b/scripts/package-build/kea/patches/isc-kea/0001-Add-multithreading-test-mode.patch deleted file mode 100644 index 981e6f1d..00000000 --- a/scripts/package-build/kea/patches/isc-kea/0001-Add-multithreading-test-mode.patch +++ /dev/null @@ -1,135 +0,0 @@ -From cb2b064162e2d5bf09331c619abf76a40130ade1 Mon Sep 17 00:00:00 2001 -From: sarthurdev <s.arthur@vyos.io> -Date: Wed, 2 Apr 2025 08:48:48 +0000 -Subject: [PATCH 1/2] Add multithreading test mode - ---- - src/bin/dhcp4/json_config_parser.cc | 4 ++++ - src/bin/dhcp6/json_config_parser.cc | 6 +++++- - src/lib/config/cmd_http_listener.cc | 3 +++ - src/lib/tcp/mt_tcp_listener_mgr.cc | 3 +++ - src/lib/util/multi_threading_mgr.cc | 3 ++- - src/lib/util/multi_threading_mgr.h | 19 +++++++++++++++++++ - 6 files changed, 36 insertions(+), 2 deletions(-) - -diff --git a/src/bin/dhcp4/json_config_parser.cc b/src/bin/dhcp4/json_config_parser.cc -index c2e34c5..1350816 100644 ---- a/src/bin/dhcp4/json_config_parser.cc -+++ b/src/bin/dhcp4/json_config_parser.cc -@@ -718,6 +718,10 @@ configureDhcp4Server(Dhcpv4Srv& server, isc::data::ConstElementPtr config_set, - LOG_DEBUG(dhcp4_logger, DBG_DHCP4_COMMAND, DHCP4_CONFIG_START) - .arg(server.redactConfig(config_set)->str()); - -+ if (check_only) { -+ MultiThreadingMgr::instance().setTestMode(true); -+ } -+ - auto answer = processDhcp4Config(config_set); - - int status_code = CONTROL_RESULT_SUCCESS; -diff --git a/src/bin/dhcp6/json_config_parser.cc b/src/bin/dhcp6/json_config_parser.cc -index 671d69a..a74a568 100644 ---- a/src/bin/dhcp6/json_config_parser.cc -+++ b/src/bin/dhcp6/json_config_parser.cc -@@ -850,6 +850,10 @@ configureDhcp6Server(Dhcpv6Srv& server, isc::data::ConstElementPtr config_set, - LOG_DEBUG(dhcp6_logger, DBG_DHCP6_COMMAND, DHCP6_CONFIG_START) - .arg(server.redactConfig(config_set)->str()); - -+ if (check_only) { -+ MultiThreadingMgr::instance().setTestMode(true); -+ } -+ - auto answer = processDhcp6Config(config_set); - - int status_code = CONTROL_RESULT_SUCCESS; -@@ -953,7 +957,7 @@ configureDhcp6Server(Dhcpv6Srv& server, isc::data::ConstElementPtr config_set, - // configuration. This will add created subnets and option values into - // the server's configuration. - // This operation should be exception safe but let's make sure. -- if (status_code == CONTROL_RESULT_SUCCESS && (!check_only || extra_checks)) { -+ if (status_code == CONTROL_RESULT_SUCCESS && !check_only) { - try { - - // Setup the command channel. -diff --git a/src/lib/config/cmd_http_listener.cc b/src/lib/config/cmd_http_listener.cc -index 9dfea59..394806e 100644 ---- a/src/lib/config/cmd_http_listener.cc -+++ b/src/lib/config/cmd_http_listener.cc -@@ -40,6 +40,9 @@ CmdHttpListener::~CmdHttpListener() { - - void - CmdHttpListener::start() { -+ if (MultiThreadingMgr::instance().isTestMode()) { -+ return; -+ } - // We must be in multi-threading mode. - if (!MultiThreadingMgr::instance().getMode()) { - isc_throw(InvalidOperation, "CmdHttpListener cannot be started" -diff --git a/src/lib/tcp/mt_tcp_listener_mgr.cc b/src/lib/tcp/mt_tcp_listener_mgr.cc -index e880284..4680717 100644 ---- a/src/lib/tcp/mt_tcp_listener_mgr.cc -+++ b/src/lib/tcp/mt_tcp_listener_mgr.cc -@@ -40,6 +40,9 @@ MtTcpListenerMgr::~MtTcpListenerMgr() { - - void - MtTcpListenerMgr::start() { -+ if (MultiThreadingMgr::instance().isTestMode()) { -+ return; -+ } - // We must be in multi-threading mode. - if (!MultiThreadingMgr::instance().getMode()) { - isc_throw(InvalidOperation, "MtTcpListenerMgr cannot be started" -diff --git a/src/lib/util/multi_threading_mgr.cc b/src/lib/util/multi_threading_mgr.cc -index d1526b9..cab284d 100644 ---- a/src/lib/util/multi_threading_mgr.cc -+++ b/src/lib/util/multi_threading_mgr.cc -@@ -14,7 +14,8 @@ namespace isc { - namespace util { - - MultiThreadingMgr::MultiThreadingMgr() -- : enabled_(false), critical_section_count_(0), thread_pool_size_(0) { -+ : enabled_(false), test_mode_(false), critical_section_count_(0), -+ thread_pool_size_(0) { - } - - MultiThreadingMgr::~MultiThreadingMgr() { -diff --git a/src/lib/util/multi_threading_mgr.h b/src/lib/util/multi_threading_mgr.h -index e86c488..f3da67b 100644 ---- a/src/lib/util/multi_threading_mgr.h -+++ b/src/lib/util/multi_threading_mgr.h -@@ -154,6 +154,22 @@ public: - /// @param enabled The new mode. - void setMode(bool enabled); - -+ /// @brief Sets or clears the test mode for @c MultiThreadingMgr. -+ /// -+ /// @param test_mode A flag which indicates that the @c MultiThreadingMgr is -+ /// in the test mode (if true), or not (if false). -+ void setTestMode(const bool test_mode) { -+ test_mode_ = test_mode; -+ } -+ -+ /// @brief Checks if the @c MultiThreadingMgr is in the test mode. -+ /// -+ /// @return true if the @c MultiThreadingMgr is in the test mode, false -+ /// otherwise. -+ bool isTestMode() const { -+ return (test_mode_); -+ } -+ - /// @brief Enter critical section. - /// - /// When entering @ref MultiThreadingCriticalSection, increment internal -@@ -308,6 +324,9 @@ private: - /// otherwise. - bool enabled_; - -+ /// @brief Indicates if the @c MultiThreadingMgr is in the test mode. -+ bool test_mode_; -+ - /// @brief The critical section count. - /// - /// In case the configuration is applied within a --- -2.39.5 - diff --git a/scripts/package-build/kea/patches/isc-kea/0002-Add-ping_check-hook-library.patch b/scripts/package-build/kea/patches/isc-kea/0002-Add-ping_check-hook-library.patch deleted file mode 100644 index c2f172ca..00000000 --- a/scripts/package-build/kea/patches/isc-kea/0002-Add-ping_check-hook-library.patch +++ /dev/null @@ -1,13277 +0,0 @@ -From 6f198a187195a7fa4ad2cf9d147532bd64724f65 Mon Sep 17 00:00:00 2001 -From: sarthurdev <965089+sarthurdev@users.noreply.github.com> -Date: Mon, 24 Mar 2025 19:38:34 +0100 -Subject: [PATCH] Add ping_check hook library - ---- - configure.ac | 3 + - src/hooks/dhcp/Makefile.am | 2 +- - src/hooks/dhcp/ping_check/Doxyfile | 2568 +++++++++++++++++ - src/hooks/dhcp/ping_check/Makefile.am | 104 + - src/hooks/dhcp/ping_check/config_cache.cc | 107 + - src/hooks/dhcp/ping_check/config_cache.h | 146 + - src/hooks/dhcp/ping_check/icmp_endpoint.h | 134 + - src/hooks/dhcp/ping_check/icmp_msg.cc | 112 + - src/hooks/dhcp/ping_check/icmp_msg.h | 223 ++ - src/hooks/dhcp/ping_check/icmp_socket.h | 359 +++ - .../dhcp/ping_check/libloadtests/.gitignore | 1 + - .../dhcp/ping_check/libloadtests/Makefile.am | 60 + - .../libloadtests/load_unload_unittests.cc | 107 + - .../dhcp/ping_check/libloadtests/meson.build | 21 + - .../ping_check/libloadtests/run_unittests.cc | 19 + - src/hooks/dhcp/ping_check/meson.build | 41 + - src/hooks/dhcp/ping_check/ping_channel.cc | 466 +++ - src/hooks/dhcp/ping_check/ping_channel.h | 371 +++ - src/hooks/dhcp/ping_check/ping_check.dox | 44 + - .../dhcp/ping_check/ping_check_callouts.cc | 240 ++ - .../dhcp/ping_check/ping_check_config.cc | 98 + - src/hooks/dhcp/ping_check/ping_check_config.h | 134 + - src/hooks/dhcp/ping_check/ping_check_log.cc | 17 + - src/hooks/dhcp/ping_check/ping_check_log.h | 23 + - .../dhcp/ping_check/ping_check_messages.cc | 99 + - .../dhcp/ping_check/ping_check_messages.h | 50 + - .../dhcp/ping_check/ping_check_messages.mes | 229 ++ - src/hooks/dhcp/ping_check/ping_check_mgr.cc | 798 +++++ - src/hooks/dhcp/ping_check/ping_check_mgr.h | 436 +++ - src/hooks/dhcp/ping_check/ping_context.cc | 237 ++ - src/hooks/dhcp/ping_check/ping_context.h | 280 ++ - .../dhcp/ping_check/ping_context_store.cc | 144 + - .../dhcp/ping_check/ping_context_store.h | 240 ++ - src/hooks/dhcp/ping_check/tests/.gitignore | 1 + - src/hooks/dhcp/ping_check/tests/Makefile.am | 70 + - .../tests/config_cache_unittests.cc | 245 ++ - .../tests/icmp_endpoint_unittests.cc | 44 + - .../ping_check/tests/icmp_msg_unittests.cc | 172 ++ - .../ping_check/tests/icmp_socket_unittests.cc | 380 +++ - src/hooks/dhcp/ping_check/tests/meson.build | 21 + - .../tests/ping_channel_unittests.cc | 821 ++++++ - .../tests/ping_check_config_unittests.cc | 287 ++ - .../tests/ping_check_mgr_unittests.cc | 1878 ++++++++++++ - .../tests/ping_context_store_unittests.cc | 467 +++ - .../tests/ping_context_unittests.cc | 146 + - .../dhcp/ping_check/tests/ping_test_utils.h | 396 +++ - .../dhcp/ping_check/tests/run_unittests.cc | 19 + - src/hooks/dhcp/ping_check/version.cc | 17 + - 48 files changed, 12876 insertions(+), 1 deletion(-) - create mode 100644 src/hooks/dhcp/ping_check/Doxyfile - create mode 100644 src/hooks/dhcp/ping_check/Makefile.am - create mode 100644 src/hooks/dhcp/ping_check/config_cache.cc - create mode 100644 src/hooks/dhcp/ping_check/config_cache.h - create mode 100644 src/hooks/dhcp/ping_check/icmp_endpoint.h - create mode 100644 src/hooks/dhcp/ping_check/icmp_msg.cc - create mode 100644 src/hooks/dhcp/ping_check/icmp_msg.h - create mode 100644 src/hooks/dhcp/ping_check/icmp_socket.h - create mode 100644 src/hooks/dhcp/ping_check/libloadtests/.gitignore - create mode 100644 src/hooks/dhcp/ping_check/libloadtests/Makefile.am - create mode 100644 src/hooks/dhcp/ping_check/libloadtests/load_unload_unittests.cc - create mode 100644 src/hooks/dhcp/ping_check/libloadtests/meson.build - create mode 100644 src/hooks/dhcp/ping_check/libloadtests/run_unittests.cc - create mode 100644 src/hooks/dhcp/ping_check/meson.build - create mode 100644 src/hooks/dhcp/ping_check/ping_channel.cc - create mode 100644 src/hooks/dhcp/ping_check/ping_channel.h - create mode 100644 src/hooks/dhcp/ping_check/ping_check.dox - create mode 100644 src/hooks/dhcp/ping_check/ping_check_callouts.cc - create mode 100644 src/hooks/dhcp/ping_check/ping_check_config.cc - create mode 100644 src/hooks/dhcp/ping_check/ping_check_config.h - create mode 100644 src/hooks/dhcp/ping_check/ping_check_log.cc - create mode 100644 src/hooks/dhcp/ping_check/ping_check_log.h - create mode 100644 src/hooks/dhcp/ping_check/ping_check_messages.cc - create mode 100644 src/hooks/dhcp/ping_check/ping_check_messages.h - create mode 100644 src/hooks/dhcp/ping_check/ping_check_messages.mes - create mode 100644 src/hooks/dhcp/ping_check/ping_check_mgr.cc - create mode 100644 src/hooks/dhcp/ping_check/ping_check_mgr.h - create mode 100644 src/hooks/dhcp/ping_check/ping_context.cc - create mode 100644 src/hooks/dhcp/ping_check/ping_context.h - create mode 100644 src/hooks/dhcp/ping_check/ping_context_store.cc - create mode 100644 src/hooks/dhcp/ping_check/ping_context_store.h - create mode 100644 src/hooks/dhcp/ping_check/tests/.gitignore - create mode 100644 src/hooks/dhcp/ping_check/tests/Makefile.am - create mode 100644 src/hooks/dhcp/ping_check/tests/config_cache_unittests.cc - create mode 100644 src/hooks/dhcp/ping_check/tests/icmp_endpoint_unittests.cc - create mode 100644 src/hooks/dhcp/ping_check/tests/icmp_msg_unittests.cc - create mode 100644 src/hooks/dhcp/ping_check/tests/icmp_socket_unittests.cc - create mode 100644 src/hooks/dhcp/ping_check/tests/meson.build - create mode 100644 src/hooks/dhcp/ping_check/tests/ping_channel_unittests.cc - create mode 100644 src/hooks/dhcp/ping_check/tests/ping_check_config_unittests.cc - create mode 100644 src/hooks/dhcp/ping_check/tests/ping_check_mgr_unittests.cc - create mode 100644 src/hooks/dhcp/ping_check/tests/ping_context_store_unittests.cc - create mode 100644 src/hooks/dhcp/ping_check/tests/ping_context_unittests.cc - create mode 100644 src/hooks/dhcp/ping_check/tests/ping_test_utils.h - create mode 100644 src/hooks/dhcp/ping_check/tests/run_unittests.cc - create mode 100644 src/hooks/dhcp/ping_check/version.cc - -diff --git a/configure.ac b/configure.ac -index cc1b31af71..23c8eefb81 100644 ---- a/configure.ac -+++ b/configure.ac -@@ -1582,6 +1582,9 @@ AC_CONFIG_FILES([src/hooks/dhcp/lease_cmds/tests/Makefile]) - AC_CONFIG_FILES([src/hooks/dhcp/mysql_cb/Makefile]) - AC_CONFIG_FILES([src/hooks/dhcp/mysql_cb/libloadtests/Makefile]) - AC_CONFIG_FILES([src/hooks/dhcp/mysql_cb/tests/Makefile]) -+AC_CONFIG_FILES([src/hooks/dhcp/ping_check/Makefile]) -+AC_CONFIG_FILES([src/hooks/dhcp/ping_check/libloadtests/Makefile]) -+AC_CONFIG_FILES([src/hooks/dhcp/ping_check/tests/Makefile]) - AC_CONFIG_FILES([src/hooks/dhcp/pgsql_cb/Makefile]) - AC_CONFIG_FILES([src/hooks/dhcp/pgsql_cb/libloadtests/Makefile]) - AC_CONFIG_FILES([src/hooks/dhcp/pgsql_cb/tests/Makefile]) -diff --git a/src/hooks/dhcp/Makefile.am b/src/hooks/dhcp/Makefile.am -index 1b77976424..806e310a17 100644 ---- a/src/hooks/dhcp/Makefile.am -+++ b/src/hooks/dhcp/Makefile.am -@@ -8,4 +8,4 @@ if HAVE_PGSQL - SUBDIRS += pgsql_cb - endif - --SUBDIRS += run_script stat_cmds user_chk -+SUBDIRS += run_script stat_cmds user_chk ping_check -diff --git a/src/hooks/dhcp/ping_check/Doxyfile b/src/hooks/dhcp/ping_check/Doxyfile -new file mode 100644 -index 0000000000..7c8554b557 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/Doxyfile -@@ -0,0 +1,2568 @@ -+# Doxyfile 1.9.1 -+ -+# This file describes the settings to be used by the documentation system -+# doxygen (www.doxygen.org) for a project. -+# -+# All text after a double hash (##) is considered a comment and is placed in -+# front of the TAG it is preceding. -+# -+# All text after a single hash (#) is considered a comment and will be ignored. -+# The format is: -+# TAG = value [value, ...] -+# For lists, items can also be appended using: -+# TAG += value [value, ...] -+# Values that contain spaces should be placed between quotes (\" \"). -+ -+#--------------------------------------------------------------------------- -+# Project related configuration options -+#--------------------------------------------------------------------------- -+ -+# This tag specifies the encoding used for all characters in the configuration -+# file that follow. The default is UTF-8 which is also the encoding used for all -+# text before the first occurrence of this tag. Doxygen uses libiconv (or the -+# iconv built into libc) for the transcoding. See -+# https://www.gnu.org/software/libiconv/ for the list of possible encodings. -+# The default value is: UTF-8. -+ -+DOXYFILE_ENCODING = UTF-8 -+ -+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by -+# double-quotes, unless you are using Doxywizard) that should identify the -+# project for which the documentation is generated. This name is used in the -+# title of most generated pages and in a few other places. -+# The default value is: My Project. -+ -+PROJECT_NAME = "Kea Ping Check Hooks Library" -+ -+# The PROJECT_NUMBER tag can be used to enter a project or revision number. This -+# could be handy for archiving the generated documentation or if some version -+# control system is used. -+ -+PROJECT_NUMBER = -+ -+# Using the PROJECT_BRIEF tag one can provide an optional one line description -+# for a project that appears at the top of each page and should give viewer a -+# quick idea about the purpose of the project. Keep the description short. -+ -+PROJECT_BRIEF = -+ -+# With the PROJECT_LOGO tag one can specify a logo or an icon that is included -+# in the documentation. The maximum height of the logo should not exceed 55 -+# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy -+# the logo to the output directory. -+ -+PROJECT_LOGO = ../../../../../doc/images/kea-logo-100x70.png -+ -+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path -+# into which the generated documentation will be written. If a relative path is -+# entered, it will be relative to the location where doxygen was started. If -+# left blank the current directory will be used. -+ -+OUTPUT_DIRECTORY = html -+ -+# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- -+# directories (in 2 levels) under the output directory of each output format and -+# will distribute the generated files over these directories. Enabling this -+# option can be useful when feeding doxygen a huge amount of source files, where -+# putting all generated files in the same directory would otherwise causes -+# performance problems for the file system. -+# The default value is: NO. -+ -+CREATE_SUBDIRS = YES -+ -+# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII -+# characters to appear in the names of generated files. If set to NO, non-ASCII -+# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode -+# U+3044. -+# The default value is: NO. -+ -+ALLOW_UNICODE_NAMES = NO -+ -+# The OUTPUT_LANGUAGE tag is used to specify the language in which all -+# documentation generated by doxygen is written. Doxygen will use this -+# information to generate all constant output in the proper language. -+# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, -+# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), -+# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, -+# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), -+# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, -+# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, -+# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, -+# Ukrainian and Vietnamese. -+# The default value is: English. -+ -+OUTPUT_LANGUAGE = English -+ -+# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all -+# documentation generated by doxygen is written. Doxygen will use this -+# information to generate all generated output in the proper direction. -+# Possible values are: None, LTR, RTL and Context. -+# The default value is: None. -+ -+OUTPUT_TEXT_DIRECTION = None -+ -+# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member -+# descriptions after the members that are listed in the file and class -+# documentation (similar to Javadoc). Set to NO to disable this. -+# The default value is: YES. -+ -+BRIEF_MEMBER_DESC = YES -+ -+# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief -+# description of a member or function before the detailed description -+# -+# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the -+# brief descriptions will be completely suppressed. -+# The default value is: YES. -+ -+REPEAT_BRIEF = YES -+ -+# This tag implements a quasi-intelligent brief description abbreviator that is -+# used to form the text in various listings. Each string in this list, if found -+# as the leading text of the brief description, will be stripped from the text -+# and the result, after processing the whole list, is used as the annotated -+# text. Otherwise, the brief description is used as-is. If left blank, the -+# following values are used ($name is automatically replaced with the name of -+# the entity):The $name class, The $name widget, The $name file, is, provides, -+# specifies, contains, represents, a, an and the. -+ -+ABBREVIATE_BRIEF = -+ -+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then -+# doxygen will generate a detailed section even if there is only a brief -+# description. -+# The default value is: NO. -+ -+ALWAYS_DETAILED_SEC = NO -+ -+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all -+# inherited members of a class in the documentation of that class as if those -+# members were ordinary class members. Constructors, destructors and assignment -+# operators of the base classes will not be shown. -+# The default value is: NO. -+ -+INLINE_INHERITED_MEMB = NO -+ -+# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path -+# before files name in the file list and in the header files. If set to NO the -+# shortest path that makes the file name unique will be used -+# The default value is: YES. -+ -+FULL_PATH_NAMES = NO -+ -+# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. -+# Stripping is only done if one of the specified strings matches the left-hand -+# part of the path. The tag can be used to show relative paths in the file list. -+# If left blank the directory from which doxygen is run is used as the path to -+# strip. -+# -+# Note that you can specify absolute paths here, but also relative paths, which -+# will be relative from the directory where doxygen is started. -+# This tag requires that the tag FULL_PATH_NAMES is set to YES. -+ -+STRIP_FROM_PATH = -+ -+# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the -+# path mentioned in the documentation of a class, which tells the reader which -+# header file to include in order to use a class. If left blank only the name of -+# the header file containing the class definition is used. Otherwise one should -+# specify the list of include paths that are normally passed to the compiler -+# using the -I flag. -+ -+STRIP_FROM_INC_PATH = -+ -+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but -+# less readable) file names. This can be useful is your file systems doesn't -+# support long names like on DOS, Mac, or CD-ROM. -+# The default value is: NO. -+ -+SHORT_NAMES = NO -+ -+# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the -+# first line (until the first dot) of a Javadoc-style comment as the brief -+# description. If set to NO, the Javadoc-style will behave just like regular Qt- -+# style comments (thus requiring an explicit @brief command for a brief -+# description.) -+# The default value is: NO. -+ -+JAVADOC_AUTOBRIEF = YES -+ -+# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line -+# such as -+# /*************** -+# as being the beginning of a Javadoc-style comment "banner". If set to NO, the -+# Javadoc-style will behave just like regular comments and it will not be -+# interpreted by doxygen. -+# The default value is: NO. -+ -+JAVADOC_BANNER = NO -+ -+# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first -+# line (until the first dot) of a Qt-style comment as the brief description. If -+# set to NO, the Qt-style will behave just like regular Qt-style comments (thus -+# requiring an explicit \brief command for a brief description.) -+# The default value is: NO. -+ -+QT_AUTOBRIEF = NO -+ -+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a -+# multi-line C++ special comment block (i.e. a block of //! or /// comments) as -+# a brief description. This used to be the default behavior. The new default is -+# to treat a multi-line C++ comment block as a detailed description. Set this -+# tag to YES if you prefer the old behavior instead. -+# -+# Note that setting this tag to YES also means that rational rose comments are -+# not recognized any more. -+# The default value is: NO. -+ -+MULTILINE_CPP_IS_BRIEF = NO -+ -+# By default Python docstrings are displayed as preformatted text and doxygen's -+# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the -+# doxygen's special commands can be used and the contents of the docstring -+# documentation blocks is shown as doxygen documentation. -+# The default value is: YES. -+ -+PYTHON_DOCSTRING = YES -+ -+# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the -+# documentation from any documented member that it re-implements. -+# The default value is: YES. -+ -+INHERIT_DOCS = YES -+ -+# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new -+# page for each member. If set to NO, the documentation of a member will be part -+# of the file/class/namespace that contains it. -+# The default value is: NO. -+ -+SEPARATE_MEMBER_PAGES = NO -+ -+# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen -+# uses this value to replace tabs by spaces in code fragments. -+# Minimum value: 1, maximum value: 16, default value: 4. -+ -+TAB_SIZE = 4 -+ -+# This tag can be used to specify a number of aliases that act as commands in -+# the documentation. An alias has the form: -+# name=value -+# For example adding -+# "sideeffect=@par Side Effects:\n" -+# will allow you to put the command \sideeffect (or @sideeffect) in the -+# documentation, which will result in a user-defined paragraph with heading -+# "Side Effects:". You can put \n's in the value part of an alias to insert -+# newlines (in the resulting output). You can put ^^ in the value part of an -+# alias to insert a newline as if a physical newline was in the original file. -+# When you need a literal { or } or , in the value part of an alias you have to -+# escape them by means of a backslash (\), this can lead to conflicts with the -+# commands \{ and \} for these it is advised to use the version @{ and @} or use -+# a double escape (\\{ and \\}) -+ -+ALIASES = -+ -+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources -+# only. Doxygen will then generate output that is more tailored for C. For -+# instance, some of the names that are used will be different. The list of all -+# members will be omitted, etc. -+# The default value is: NO. -+ -+OPTIMIZE_OUTPUT_FOR_C = NO -+ -+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or -+# Python sources only. Doxygen will then generate output that is more tailored -+# for that language. For instance, namespaces will be presented as packages, -+# qualified scopes will look different, etc. -+# The default value is: NO. -+ -+OPTIMIZE_OUTPUT_JAVA = NO -+ -+# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran -+# sources. Doxygen will then generate output that is tailored for Fortran. -+# The default value is: NO. -+ -+OPTIMIZE_FOR_FORTRAN = NO -+ -+# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL -+# sources. Doxygen will then generate output that is tailored for VHDL. -+# The default value is: NO. -+ -+OPTIMIZE_OUTPUT_VHDL = NO -+ -+# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice -+# sources only. Doxygen will then generate output that is more tailored for that -+# language. For instance, namespaces will be presented as modules, types will be -+# separated into more groups, etc. -+# The default value is: NO. -+ -+OPTIMIZE_OUTPUT_SLICE = NO -+ -+# Doxygen selects the parser to use depending on the extension of the files it -+# parses. With this tag you can assign which parser to use for a given -+# extension. Doxygen has a built-in mapping, but you can override or extend it -+# using this tag. The format is ext=language, where ext is a file extension, and -+# language is one of the parsers supported by doxygen: IDL, Java, JavaScript, -+# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, VHDL, -+# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: -+# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser -+# tries to guess whether the code is fixed or free formatted code, this is the -+# default for Fortran type files). For instance to make doxygen treat .inc files -+# as Fortran files (default is PHP), and .f files as C (default is Fortran), -+# use: inc=Fortran f=C. -+# -+# Note: For files without extension you can use no_extension as a placeholder. -+# -+# Note that for custom extensions you also need to set FILE_PATTERNS otherwise -+# the files are not read by doxygen. When specifying no_extension you should add -+# * to the FILE_PATTERNS. -+# -+# Note see also the list of default file extension mappings. -+ -+EXTENSION_MAPPING = -+ -+# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments -+# according to the Markdown format, which allows for more readable -+# documentation. See https://daringfireball.net/projects/markdown/ for details. -+# The output of markdown processing is further processed by doxygen, so you can -+# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in -+# case of backward compatibilities issues. -+# The default value is: YES. -+ -+MARKDOWN_SUPPORT = YES -+ -+# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up -+# to that level are automatically included in the table of contents, even if -+# they do not have an id attribute. -+# Note: This feature currently applies only to Markdown headings. -+# Minimum value: 0, maximum value: 99, default value: 5. -+# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. -+ -+TOC_INCLUDE_HEADINGS = 5 -+ -+# When enabled doxygen tries to link words that correspond to documented -+# classes, or namespaces to their corresponding documentation. Such a link can -+# be prevented in individual cases by putting a % sign in front of the word or -+# globally by setting AUTOLINK_SUPPORT to NO. -+# The default value is: YES. -+ -+AUTOLINK_SUPPORT = YES -+ -+# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want -+# to include (a tag file for) the STL sources as input, then you should set this -+# tag to YES in order to let doxygen match functions declarations and -+# definitions whose arguments contain STL classes (e.g. func(std::string); -+# versus func(std::string) {}). This also make the inheritance and collaboration -+# diagrams that involve STL classes more complete and accurate. -+# The default value is: NO. -+ -+BUILTIN_STL_SUPPORT = YES -+ -+# If you use Microsoft's C++/CLI language, you should set this option to YES to -+# enable parsing support. -+# The default value is: NO. -+ -+CPP_CLI_SUPPORT = NO -+ -+# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: -+# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen -+# will parse them like normal C++ but will assume all classes use public instead -+# of private inheritance when no explicit protection keyword is present. -+# The default value is: NO. -+ -+SIP_SUPPORT = NO -+ -+# For Microsoft's IDL there are propget and propput attributes to indicate -+# getter and setter methods for a property. Setting this option to YES will make -+# doxygen to replace the get and set methods by a property in the documentation. -+# This will only work if the methods are indeed getting or setting a simple -+# type. If this is not the case, or you want to show the methods anyway, you -+# should set this option to NO. -+# The default value is: YES. -+ -+IDL_PROPERTY_SUPPORT = YES -+ -+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC -+# tag is set to YES then doxygen will reuse the documentation of the first -+# member in the group (if any) for the other members of the group. By default -+# all members of a group must be documented explicitly. -+# The default value is: NO. -+ -+DISTRIBUTE_GROUP_DOC = NO -+ -+# If one adds a struct or class to a group and this option is enabled, then also -+# any nested class or struct is added to the same group. By default this option -+# is disabled and one has to add nested compounds explicitly via \ingroup. -+# The default value is: NO. -+ -+GROUP_NESTED_COMPOUNDS = NO -+ -+# Set the SUBGROUPING tag to YES to allow class member groups of the same type -+# (for instance a group of public functions) to be put as a subgroup of that -+# type (e.g. under the Public Functions section). Set it to NO to prevent -+# subgrouping. Alternatively, this can be done per class using the -+# \nosubgrouping command. -+# The default value is: YES. -+ -+SUBGROUPING = YES -+ -+# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions -+# are shown inside the group in which they are included (e.g. using \ingroup) -+# instead of on a separate page (for HTML and Man pages) or section (for LaTeX -+# and RTF). -+# -+# Note that this feature does not work in combination with -+# SEPARATE_MEMBER_PAGES. -+# The default value is: NO. -+ -+INLINE_GROUPED_CLASSES = NO -+ -+# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions -+# with only public data fields or simple typedef fields will be shown inline in -+# the documentation of the scope in which they are defined (i.e. file, -+# namespace, or group documentation), provided this scope is documented. If set -+# to NO, structs, classes, and unions are shown on a separate page (for HTML and -+# Man pages) or section (for LaTeX and RTF). -+# The default value is: NO. -+ -+INLINE_SIMPLE_STRUCTS = NO -+ -+# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or -+# enum is documented as struct, union, or enum with the name of the typedef. So -+# typedef struct TypeS {} TypeT, will appear in the documentation as a struct -+# with name TypeT. When disabled the typedef will appear as a member of a file, -+# namespace, or class. And the struct will be named TypeS. This can typically be -+# useful for C code in case the coding convention dictates that all compound -+# types are typedef'ed and only the typedef is referenced, never the tag name. -+# The default value is: NO. -+ -+TYPEDEF_HIDES_STRUCT = NO -+ -+# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This -+# cache is used to resolve symbols given their name and scope. Since this can be -+# an expensive process and often the same symbol appears multiple times in the -+# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small -+# doxygen will become slower. If the cache is too large, memory is wasted. The -+# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range -+# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 -+# symbols. At the end of a run doxygen will report the cache usage and suggest -+# the optimal cache size from a speed point of view. -+# Minimum value: 0, maximum value: 9, default value: 0. -+ -+LOOKUP_CACHE_SIZE = 0 -+ -+# The NUM_PROC_THREADS specifies the number threads doxygen is allowed to use -+# during processing. When set to 0 doxygen will based this on the number of -+# cores available in the system. You can set it explicitly to a value larger -+# than 0 to get more control over the balance between CPU load and processing -+# speed. At this moment only the input processing can be done using multiple -+# threads. Since this is still an experimental feature the default is set to 1, -+# which effectively disables parallel processing. Please report any issues you -+# encounter. Generating dot graphs in parallel is controlled by the -+# DOT_NUM_THREADS setting. -+# Minimum value: 0, maximum value: 32, default value: 1. -+ -+NUM_PROC_THREADS = 1 -+ -+#--------------------------------------------------------------------------- -+# Build related configuration options -+#--------------------------------------------------------------------------- -+ -+# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in -+# documentation are documented, even if no documentation was available. Private -+# class members and static file members will be hidden unless the -+# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. -+# Note: This will also disable the warnings about undocumented members that are -+# normally produced when WARNINGS is set to YES. -+# The default value is: NO. -+ -+EXTRACT_ALL = YES -+ -+# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will -+# be included in the documentation. -+# The default value is: NO. -+ -+EXTRACT_PRIVATE = NO -+ -+# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual -+# methods of a class will be included in the documentation. -+# The default value is: NO. -+ -+EXTRACT_PRIV_VIRTUAL = NO -+ -+# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal -+# scope will be included in the documentation. -+# The default value is: NO. -+ -+EXTRACT_PACKAGE = NO -+ -+# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be -+# included in the documentation. -+# The default value is: NO. -+ -+EXTRACT_STATIC = NO -+ -+# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined -+# locally in source files will be included in the documentation. If set to NO, -+# only classes defined in header files are included. Does not have any effect -+# for Java sources. -+# The default value is: YES. -+ -+EXTRACT_LOCAL_CLASSES = YES -+ -+# This flag is only useful for Objective-C code. If set to YES, local methods, -+# which are defined in the implementation section but not in the interface are -+# included in the documentation. If set to NO, only methods in the interface are -+# included. -+# The default value is: NO. -+ -+EXTRACT_LOCAL_METHODS = NO -+ -+# If this flag is set to YES, the members of anonymous namespaces will be -+# extracted and appear in the documentation as a namespace called -+# 'anonymous_namespace{file}', where file will be replaced with the base name of -+# the file that contains the anonymous namespace. By default anonymous namespace -+# are hidden. -+# The default value is: NO. -+ -+EXTRACT_ANON_NSPACES = NO -+ -+# If this flag is set to YES, the name of an unnamed parameter in a declaration -+# will be determined by the corresponding definition. By default unnamed -+# parameters remain unnamed in the output. -+# The default value is: YES. -+ -+RESOLVE_UNNAMED_PARAMS = YES -+ -+# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all -+# undocumented members inside documented classes or files. If set to NO these -+# members will be included in the various overviews, but no documentation -+# section is generated. This option has no effect if EXTRACT_ALL is enabled. -+# The default value is: NO. -+ -+HIDE_UNDOC_MEMBERS = NO -+ -+# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all -+# undocumented classes that are normally visible in the class hierarchy. If set -+# to NO, these classes will be included in the various overviews. This option -+# has no effect if EXTRACT_ALL is enabled. -+# The default value is: NO. -+ -+HIDE_UNDOC_CLASSES = NO -+ -+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend -+# declarations. If set to NO, these declarations will be included in the -+# documentation. -+# The default value is: NO. -+ -+HIDE_FRIEND_COMPOUNDS = NO -+ -+# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any -+# documentation blocks found inside the body of a function. If set to NO, these -+# blocks will be appended to the function's detailed documentation block. -+# The default value is: NO. -+ -+HIDE_IN_BODY_DOCS = NO -+ -+# The INTERNAL_DOCS tag determines if documentation that is typed after a -+# \internal command is included. If the tag is set to NO then the documentation -+# will be excluded. Set it to YES to include the internal documentation. -+# The default value is: NO. -+ -+INTERNAL_DOCS = NO -+ -+# With the correct setting of option CASE_SENSE_NAMES doxygen will better be -+# able to match the capabilities of the underlying filesystem. In case the -+# filesystem is case sensitive (i.e. it supports files in the same directory -+# whose names only differ in casing), the option must be set to YES to properly -+# deal with such files in case they appear in the input. For filesystems that -+# are not case sensitive the option should be be set to NO to properly deal with -+# output files written for symbols that only differ in casing, such as for two -+# classes, one named CLASS and the other named Class, and to also support -+# references to files without having to specify the exact matching casing. On -+# Windows (including Cygwin) and MacOS, users should typically set this option -+# to NO, whereas on Linux or other Unix flavors it should typically be set to -+# YES. -+# The default value is: system dependent. -+ -+CASE_SENSE_NAMES = YES -+ -+# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with -+# their full class and namespace scopes in the documentation. If set to YES, the -+# scope will be hidden. -+# The default value is: NO. -+ -+HIDE_SCOPE_NAMES = NO -+ -+# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will -+# append additional text to a page's title, such as Class Reference. If set to -+# YES the compound reference will be hidden. -+# The default value is: NO. -+ -+HIDE_COMPOUND_REFERENCE= NO -+ -+# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of -+# the files that are included by a file in the documentation of that file. -+# The default value is: YES. -+ -+SHOW_INCLUDE_FILES = YES -+ -+# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each -+# grouped member an include statement to the documentation, telling the reader -+# which file to include in order to use the member. -+# The default value is: NO. -+ -+SHOW_GROUPED_MEMB_INC = NO -+ -+# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include -+# files with double quotes in the documentation rather than with sharp brackets. -+# The default value is: NO. -+ -+FORCE_LOCAL_INCLUDES = NO -+ -+# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the -+# documentation for inline members. -+# The default value is: YES. -+ -+INLINE_INFO = YES -+ -+# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the -+# (detailed) documentation of file and class members alphabetically by member -+# name. If set to NO, the members will appear in declaration order. -+# The default value is: YES. -+ -+SORT_MEMBER_DOCS = YES -+ -+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief -+# descriptions of file, namespace and class members alphabetically by member -+# name. If set to NO, the members will appear in declaration order. Note that -+# this will also influence the order of the classes in the class list. -+# The default value is: NO. -+ -+SORT_BRIEF_DOCS = YES -+ -+# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the -+# (brief and detailed) documentation of class members so that constructors and -+# destructors are listed first. If set to NO the constructors will appear in the -+# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. -+# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief -+# member documentation. -+# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting -+# detailed member documentation. -+# The default value is: NO. -+ -+SORT_MEMBERS_CTORS_1ST = YES -+ -+# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy -+# of group names into alphabetical order. If set to NO the group names will -+# appear in their defined order. -+# The default value is: NO. -+ -+SORT_GROUP_NAMES = YES -+ -+# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by -+# fully-qualified names, including namespaces. If set to NO, the class list will -+# be sorted only by class name, not including the namespace part. -+# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. -+# Note: This option applies only to the class list, not to the alphabetical -+# list. -+# The default value is: NO. -+ -+SORT_BY_SCOPE_NAME = NO -+ -+# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper -+# type resolution of all parameters of a function it will reject a match between -+# the prototype and the implementation of a member function even if there is -+# only one candidate or it is obvious which candidate to choose by doing a -+# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still -+# accept a match between prototype and implementation in such cases. -+# The default value is: NO. -+ -+STRICT_PROTO_MATCHING = NO -+ -+# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo -+# list. This list is created by putting \todo commands in the documentation. -+# The default value is: YES. -+ -+GENERATE_TODOLIST = YES -+ -+# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test -+# list. This list is created by putting \test commands in the documentation. -+# The default value is: YES. -+ -+GENERATE_TESTLIST = YES -+ -+# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug -+# list. This list is created by putting \bug commands in the documentation. -+# The default value is: YES. -+ -+GENERATE_BUGLIST = YES -+ -+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) -+# the deprecated list. This list is created by putting \deprecated commands in -+# the documentation. -+# The default value is: YES. -+ -+GENERATE_DEPRECATEDLIST= YES -+ -+# The ENABLED_SECTIONS tag can be used to enable conditional documentation -+# sections, marked by \if <section_label> ... \endif and \cond <section_label> -+# ... \endcond blocks. -+ -+ENABLED_SECTIONS = -+ -+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the -+# initial value of a variable or macro / define can have for it to appear in the -+# documentation. If the initializer consists of more lines than specified here -+# it will be hidden. Use a value of 0 to hide initializers completely. The -+# appearance of the value of individual variables and macros / defines can be -+# controlled using \showinitializer or \hideinitializer command in the -+# documentation regardless of this setting. -+# Minimum value: 0, maximum value: 10000, default value: 30. -+ -+MAX_INITIALIZER_LINES = 30 -+ -+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at -+# the bottom of the documentation of classes and structs. If set to YES, the -+# list will mention the files that were used to generate the documentation. -+# The default value is: YES. -+ -+SHOW_USED_FILES = YES -+ -+# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This -+# will remove the Files entry from the Quick Index and from the Folder Tree View -+# (if specified). -+# The default value is: YES. -+ -+SHOW_FILES = YES -+ -+# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces -+# page. This will remove the Namespaces entry from the Quick Index and from the -+# Folder Tree View (if specified). -+# The default value is: YES. -+ -+SHOW_NAMESPACES = YES -+ -+# The FILE_VERSION_FILTER tag can be used to specify a program or script that -+# doxygen should invoke to get the current version for each file (typically from -+# the version control system). Doxygen will invoke the program by executing (via -+# popen()) the command command input-file, where command is the value of the -+# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided -+# by doxygen. Whatever the program writes to standard output is used as the file -+# version. For an example see the documentation. -+ -+FILE_VERSION_FILTER = -+ -+# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed -+# by doxygen. The layout file controls the global structure of the generated -+# output files in an output format independent way. To create the layout file -+# that represents doxygen's defaults, run doxygen with the -l option. You can -+# optionally specify a file name after the option, if omitted DoxygenLayout.xml -+# will be used as the name of the layout file. -+# -+# Note that if you run doxygen from a directory containing a file called -+# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE -+# tag is left empty. -+ -+LAYOUT_FILE = -+ -+# The CITE_BIB_FILES tag can be used to specify one or more bib files containing -+# the reference definitions. This must be a list of .bib files. The .bib -+# extension is automatically appended if omitted. This requires the bibtex tool -+# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info. -+# For LaTeX the style of the bibliography can be controlled using -+# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the -+# search path. See also \cite for info how to create references. -+ -+CITE_BIB_FILES = -+ -+#--------------------------------------------------------------------------- -+# Configuration options related to warning and progress messages -+#--------------------------------------------------------------------------- -+ -+# The QUIET tag can be used to turn on/off the messages that are generated to -+# standard output by doxygen. If QUIET is set to YES this implies that the -+# messages are off. -+# The default value is: NO. -+ -+QUIET = YES -+ -+# The WARNINGS tag can be used to turn on/off the warning messages that are -+# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES -+# this implies that the warnings are on. -+# -+# Tip: Turn warnings on while writing the documentation. -+# The default value is: YES. -+ -+WARNINGS = YES -+ -+# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate -+# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag -+# will automatically be disabled. -+# The default value is: YES. -+ -+WARN_IF_UNDOCUMENTED = YES -+ -+# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for -+# potential errors in the documentation, such as not documenting some parameters -+# in a documented function, or documenting parameters that don't exist or using -+# markup commands wrongly. -+# The default value is: YES. -+ -+WARN_IF_DOC_ERROR = YES -+ -+# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that -+# are documented, but have no documentation for their parameters or return -+# value. If set to NO, doxygen will only warn about wrong or incomplete -+# parameter documentation, but not about the absence of documentation. If -+# EXTRACT_ALL is set to YES then this flag will automatically be disabled. -+# The default value is: NO. -+ -+WARN_NO_PARAMDOC = NO -+ -+# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when -+# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS -+# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but -+# at the end of the doxygen process doxygen will return with a non-zero status. -+# Possible values are: NO, YES and FAIL_ON_WARNINGS. -+# The default value is: NO. -+ -+WARN_AS_ERROR = NO -+ -+# The WARN_FORMAT tag determines the format of the warning messages that doxygen -+# can produce. The string should contain the $file, $line, and $text tags, which -+# will be replaced by the file and line number from which the warning originated -+# and the warning text. Optionally the format may contain $version, which will -+# be replaced by the version of the file (if it could be obtained via -+# FILE_VERSION_FILTER) -+# The default value is: $file:$line: $text. -+ -+WARN_FORMAT = "$file:$line: $text" -+ -+# The WARN_LOGFILE tag can be used to specify a file to which warning and error -+# messages should be written. If left blank the output is written to standard -+# error (stderr). -+ -+WARN_LOGFILE = -+ -+#--------------------------------------------------------------------------- -+# Configuration options related to the input files -+#--------------------------------------------------------------------------- -+ -+# The INPUT tag is used to specify the files and/or directories that contain -+# documented source files. You may enter file names like myfile.cpp or -+# directories like /usr/src/myproject. Separate the files or directories with -+# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING -+# Note: If this tag is empty the current directory is searched. -+ -+INPUT = -+ -+# This tag can be used to specify the character encoding of the source files -+# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses -+# libiconv (or the iconv built into libc) for the transcoding. See the libiconv -+# documentation (see: -+# https://www.gnu.org/software/libiconv/) for the list of possible encodings. -+# The default value is: UTF-8. -+ -+INPUT_ENCODING = UTF-8 -+ -+# If the value of the INPUT tag contains directories, you can use the -+# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and -+# *.h) to filter out the source-files in the directories. -+# -+# Note that for custom extensions or not directly supported extensions you also -+# need to set EXTENSION_MAPPING for the extension otherwise the files are not -+# read by doxygen. -+# -+# Note the list of default checked file patterns might differ from the list of -+# default file extension mappings. -+# -+# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, -+# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, -+# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, -+# *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C comment), -+# *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, *.vhdl, -+# *.ucf, *.qsf and *.ice. -+ -+FILE_PATTERNS = *.c \ -+ *.cc \ -+ *.h \ -+ *.hpp \ -+ *.dox -+ -+# The RECURSIVE tag can be used to specify whether or not subdirectories should -+# be searched for input files as well. -+# The default value is: NO. -+ -+RECURSIVE = NO -+ -+# The EXCLUDE tag can be used to specify files and/or directories that should be -+# excluded from the INPUT source files. This way you can easily exclude a -+# subdirectory from a directory tree whose root is specified with the INPUT tag. -+# -+# Note that relative paths are relative to the directory from which doxygen is -+# run. -+ -+EXCLUDE = -+ -+# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or -+# directories that are symbolic links (a Unix file system feature) are excluded -+# from the input. -+# The default value is: NO. -+ -+EXCLUDE_SYMLINKS = NO -+ -+# If the value of the INPUT tag contains directories, you can use the -+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude -+# certain files from those directories. -+# -+# Note that the wildcards are matched against the file with absolute path, so to -+# exclude all test directories for example use the pattern */test/* -+ -+EXCLUDE_PATTERNS = -+ -+# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names -+# (namespaces, classes, functions, etc.) that should be excluded from the -+# output. The symbol name can be a fully qualified name, a word, or if the -+# wildcard * is used, a substring. Examples: ANamespace, AClass, -+# AClass::ANamespace, ANamespace::*Test -+# -+# Note that the wildcards are matched against the file with absolute path, so to -+# exclude all test directories use the pattern */test/* -+ -+EXCLUDE_SYMBOLS = -+ -+# The EXAMPLE_PATH tag can be used to specify one or more files or directories -+# that contain example code fragments that are included (see the \include -+# command). -+ -+EXAMPLE_PATH = -+ -+# If the value of the EXAMPLE_PATH tag contains directories, you can use the -+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and -+# *.h) to filter out the source-files in the directories. If left blank all -+# files are included. -+ -+EXAMPLE_PATTERNS = -+ -+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be -+# searched for input files to be used with the \include or \dontinclude commands -+# irrespective of the value of the RECURSIVE tag. -+# The default value is: NO. -+ -+EXAMPLE_RECURSIVE = NO -+ -+# The IMAGE_PATH tag can be used to specify one or more files or directories -+# that contain images that are to be included in the documentation (see the -+# \image command). -+ -+IMAGE_PATH = ../../../../../doc/images -+ -+# The INPUT_FILTER tag can be used to specify a program that doxygen should -+# invoke to filter for each input file. Doxygen will invoke the filter program -+# by executing (via popen()) the command: -+# -+# <filter> <input-file> -+# -+# where <filter> is the value of the INPUT_FILTER tag, and <input-file> is the -+# name of an input file. Doxygen will then use the output that the filter -+# program writes to standard output. If FILTER_PATTERNS is specified, this tag -+# will be ignored. -+# -+# Note that the filter must not add or remove lines; it is applied before the -+# code is scanned, but not when the output code is generated. If lines are added -+# or removed, the anchors will not be placed correctly. -+# -+# Note that for custom extensions or not directly supported extensions you also -+# need to set EXTENSION_MAPPING for the extension otherwise the files are not -+# properly processed by doxygen. -+ -+INPUT_FILTER = -+ -+# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern -+# basis. Doxygen will compare the file name with each pattern and apply the -+# filter if there is a match. The filters are a list of the form: pattern=filter -+# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how -+# filters are used. If the FILTER_PATTERNS tag is empty or if none of the -+# patterns match the file name, INPUT_FILTER is applied. -+# -+# Note that for custom extensions or not directly supported extensions you also -+# need to set EXTENSION_MAPPING for the extension otherwise the files are not -+# properly processed by doxygen. -+ -+FILTER_PATTERNS = -+ -+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using -+# INPUT_FILTER) will also be used to filter the input files that are used for -+# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). -+# The default value is: NO. -+ -+FILTER_SOURCE_FILES = NO -+ -+# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file -+# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and -+# it is also possible to disable source filtering for a specific pattern using -+# *.ext= (so without naming a filter). -+# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. -+ -+FILTER_SOURCE_PATTERNS = -+ -+# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that -+# is part of the input, its contents will be placed on the main page -+# (index.html). This can be useful if you have a project on for instance GitHub -+# and want to reuse the introduction page also for the doxygen output. -+ -+USE_MDFILE_AS_MAINPAGE = -+ -+#--------------------------------------------------------------------------- -+# Configuration options related to source browsing -+#--------------------------------------------------------------------------- -+ -+# If the SOURCE_BROWSER tag is set to YES then a list of source files will be -+# generated. Documented entities will be cross-referenced with these sources. -+# -+# Note: To get rid of all source code in the generated output, make sure that -+# also VERBATIM_HEADERS is set to NO. -+# The default value is: NO. -+ -+SOURCE_BROWSER = YES -+ -+# Setting the INLINE_SOURCES tag to YES will include the body of functions, -+# classes and enums directly into the documentation. -+# The default value is: NO. -+ -+INLINE_SOURCES = NO -+ -+# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any -+# special comment blocks from generated source code fragments. Normal C, C++ and -+# Fortran comments will always remain visible. -+# The default value is: YES. -+ -+STRIP_CODE_COMMENTS = YES -+ -+# If the REFERENCED_BY_RELATION tag is set to YES then for each documented -+# entity all documented functions referencing it will be listed. -+# The default value is: NO. -+ -+REFERENCED_BY_RELATION = YES -+ -+# If the REFERENCES_RELATION tag is set to YES then for each documented function -+# all documented entities called/used by that function will be listed. -+# The default value is: NO. -+ -+REFERENCES_RELATION = YES -+ -+# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set -+# to YES then the hyperlinks from functions in REFERENCES_RELATION and -+# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will -+# link to the documentation. -+# The default value is: YES. -+ -+REFERENCES_LINK_SOURCE = YES -+ -+# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the -+# source code will show a tooltip with additional information such as prototype, -+# brief description and links to the definition and documentation. Since this -+# will make the HTML file larger and loading of large files a bit slower, you -+# can opt to disable this feature. -+# The default value is: YES. -+# This tag requires that the tag SOURCE_BROWSER is set to YES. -+ -+SOURCE_TOOLTIPS = YES -+ -+# If the USE_HTAGS tag is set to YES then the references to source code will -+# point to the HTML generated by the htags(1) tool instead of doxygen built-in -+# source browser. The htags tool is part of GNU's global source tagging system -+# (see https://www.gnu.org/software/global/global.html). You will need version -+# 4.8.6 or higher. -+# -+# To use it do the following: -+# - Install the latest version of global -+# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file -+# - Make sure the INPUT points to the root of the source tree -+# - Run doxygen as normal -+# -+# Doxygen will invoke htags (and that will in turn invoke gtags), so these -+# tools must be available from the command line (i.e. in the search path). -+# -+# The result: instead of the source browser generated by doxygen, the links to -+# source code will now point to the output of htags. -+# The default value is: NO. -+# This tag requires that the tag SOURCE_BROWSER is set to YES. -+ -+USE_HTAGS = NO -+ -+# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a -+# verbatim copy of the header file for each class for which an include is -+# specified. Set to NO to disable this. -+# See also: Section \class. -+# The default value is: YES. -+ -+VERBATIM_HEADERS = YES -+ -+#--------------------------------------------------------------------------- -+# Configuration options related to the alphabetical class index -+#--------------------------------------------------------------------------- -+ -+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all -+# compounds will be generated. Enable this if the project contains a lot of -+# classes, structs, unions or interfaces. -+# The default value is: YES. -+ -+ALPHABETICAL_INDEX = YES -+ -+# In case all classes in a project start with a common prefix, all classes will -+# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag -+# can be used to specify a prefix (or a list of prefixes) that should be ignored -+# while generating the index headers. -+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. -+ -+IGNORE_PREFIX = -+ -+#--------------------------------------------------------------------------- -+# Configuration options related to the HTML output -+#--------------------------------------------------------------------------- -+ -+# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output -+# The default value is: YES. -+ -+GENERATE_HTML = YES -+ -+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a -+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of -+# it. -+# The default directory is: html. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+HTML_OUTPUT = ../html -+ -+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each -+# generated HTML page (for example: .htm, .php, .asp). -+# The default value is: .html. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+HTML_FILE_EXTENSION = .html -+ -+# The HTML_HEADER tag can be used to specify a user-defined HTML header file for -+# each generated HTML page. If the tag is left blank doxygen will generate a -+# standard header. -+# -+# To get valid HTML the header file that includes any scripts and style sheets -+# that doxygen needs, which is dependent on the configuration options used (e.g. -+# the setting GENERATE_TREEVIEW). It is highly recommended to start with a -+# default header using -+# doxygen -w html new_header.html new_footer.html new_stylesheet.css -+# YourConfigFile -+# and then modify the file new_header.html. See also section "Doxygen usage" -+# for information on how to generate the default header that doxygen normally -+# uses. -+# Note: The header is subject to change so you typically have to regenerate the -+# default header when upgrading to a newer version of doxygen. For a description -+# of the possible markers and block names see the documentation. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+HTML_HEADER = -+ -+# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each -+# generated HTML page. If the tag is left blank doxygen will generate a standard -+# footer. See HTML_HEADER for more information on how to generate a default -+# footer and what special commands can be used inside the footer. See also -+# section "Doxygen usage" for information on how to generate the default footer -+# that doxygen normally uses. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+HTML_FOOTER = -+ -+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style -+# sheet that is used by each HTML page. It can be used to fine-tune the look of -+# the HTML output. If left blank doxygen will generate a default style sheet. -+# See also section "Doxygen usage" for information on how to generate the style -+# sheet that doxygen normally uses. -+# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as -+# it is more robust and this tag (HTML_STYLESHEET) will in the future become -+# obsolete. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+HTML_STYLESHEET = -+ -+# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined -+# cascading style sheets that are included after the standard style sheets -+# created by doxygen. Using this option one can overrule certain style aspects. -+# This is preferred over using HTML_STYLESHEET since it does not replace the -+# standard style sheet and is therefore more robust against future updates. -+# Doxygen will copy the style sheet files to the output directory. -+# Note: The order of the extra style sheet files is of importance (e.g. the last -+# style sheet in the list overrules the setting of the previous ones in the -+# list). For an example see the documentation. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+HTML_EXTRA_STYLESHEET = -+ -+# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or -+# other source files which should be copied to the HTML output directory. Note -+# that these files will be copied to the base HTML output directory. Use the -+# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these -+# files. In the HTML_STYLESHEET file, use the file name only. Also note that the -+# files will be copied as-is; there are no commands or markers available. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+HTML_EXTRA_FILES = -+ -+# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen -+# will adjust the colors in the style sheet and background images according to -+# this color. Hue is specified as an angle on a colorwheel, see -+# https://en.wikipedia.org/wiki/Hue for more information. For instance the value -+# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 -+# purple, and 360 is red again. -+# Minimum value: 0, maximum value: 359, default value: 220. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+HTML_COLORSTYLE_HUE = 148 -+ -+# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors -+# in the HTML output. For a value of 0 the output will use grayscales only. A -+# value of 255 will produce the most vivid colors. -+# Minimum value: 0, maximum value: 255, default value: 100. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+HTML_COLORSTYLE_SAT = 93 -+ -+# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the -+# luminance component of the colors in the HTML output. Values below 100 -+# gradually make the output lighter, whereas values above 100 make the output -+# darker. The value divided by 100 is the actual gamma applied, so 80 represents -+# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not -+# change the gamma. -+# Minimum value: 40, maximum value: 240, default value: 80. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+HTML_COLORSTYLE_GAMMA = 80 -+ -+# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML -+# page will contain the date and time when the page was generated. Setting this -+# to YES can help to show when doxygen was last run and thus if the -+# documentation is up to date. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+HTML_TIMESTAMP = YES -+ -+# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML -+# documentation will contain a main index with vertical navigation menus that -+# are dynamically created via JavaScript. If disabled, the navigation index will -+# consists of multiple levels of tabs that are statically embedded in every HTML -+# page. Disable this option to support browsers that do not have JavaScript, -+# like the Qt help browser. -+# The default value is: YES. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+HTML_DYNAMIC_MENUS = YES -+ -+# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML -+# documentation will contain sections that can be hidden and shown after the -+# page has loaded. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+HTML_DYNAMIC_SECTIONS = YES -+ -+# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries -+# shown in the various tree structured indices initially; the user can expand -+# and collapse entries dynamically later on. Doxygen will expand the tree to -+# such a level that at most the specified number of entries are visible (unless -+# a fully collapsed tree already exceeds this amount). So setting the number of -+# entries 1 will produce a full collapsed tree by default. 0 is a special value -+# representing an infinite number of entries and will result in a full expanded -+# tree by default. -+# Minimum value: 0, maximum value: 9999, default value: 100. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+HTML_INDEX_NUM_ENTRIES = 100 -+ -+# If the GENERATE_DOCSET tag is set to YES, additional index files will be -+# generated that can be used as input for Apple's Xcode 3 integrated development -+# environment (see: -+# https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To -+# create a documentation set, doxygen will generate a Makefile in the HTML -+# output directory. Running make will produce the docset in that directory and -+# running make install will install the docset in -+# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at -+# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy -+# genXcode/_index.html for more information. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+GENERATE_DOCSET = NO -+ -+# This tag determines the name of the docset feed. A documentation feed provides -+# an umbrella under which multiple documentation sets from a single provider -+# (such as a company or product suite) can be grouped. -+# The default value is: Doxygen generated docs. -+# This tag requires that the tag GENERATE_DOCSET is set to YES. -+ -+DOCSET_FEEDNAME = "Doxygen generated docs" -+ -+# This tag specifies a string that should uniquely identify the documentation -+# set bundle. This should be a reverse domain-name style string, e.g. -+# com.mycompany.MyDocSet. Doxygen will append .docset to the name. -+# The default value is: org.doxygen.Project. -+# This tag requires that the tag GENERATE_DOCSET is set to YES. -+ -+DOCSET_BUNDLE_ID = org.doxygen.Project -+ -+# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify -+# the documentation publisher. This should be a reverse domain-name style -+# string, e.g. com.mycompany.MyDocSet.documentation. -+# The default value is: org.doxygen.Publisher. -+# This tag requires that the tag GENERATE_DOCSET is set to YES. -+ -+DOCSET_PUBLISHER_ID = org.doxygen.Publisher -+ -+# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. -+# The default value is: Publisher. -+# This tag requires that the tag GENERATE_DOCSET is set to YES. -+ -+DOCSET_PUBLISHER_NAME = Publisher -+ -+# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three -+# additional HTML index files: index.hhp, index.hhc, and index.hhk. The -+# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop -+# (see: -+# https://www.microsoft.com/en-us/download/details.aspx?id=21138) on Windows. -+# -+# The HTML Help Workshop contains a compiler that can convert all HTML output -+# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML -+# files are now used as the Windows 98 help format, and will replace the old -+# Windows help format (.hlp) on all Windows platforms in the future. Compressed -+# HTML files also contain an index, a table of contents, and you can search for -+# words in the documentation. The HTML workshop also contains a viewer for -+# compressed HTML files. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+GENERATE_HTMLHELP = NO -+ -+# The CHM_FILE tag can be used to specify the file name of the resulting .chm -+# file. You can add a path in front of the file if the result should not be -+# written to the html output directory. -+# This tag requires that the tag GENERATE_HTMLHELP is set to YES. -+ -+CHM_FILE = -+ -+# The HHC_LOCATION tag can be used to specify the location (absolute path -+# including file name) of the HTML help compiler (hhc.exe). If non-empty, -+# doxygen will try to run the HTML help compiler on the generated index.hhp. -+# The file has to be specified with full path. -+# This tag requires that the tag GENERATE_HTMLHELP is set to YES. -+ -+HHC_LOCATION = -+ -+# The GENERATE_CHI flag controls if a separate .chi index file is generated -+# (YES) or that it should be included in the main .chm file (NO). -+# The default value is: NO. -+# This tag requires that the tag GENERATE_HTMLHELP is set to YES. -+ -+GENERATE_CHI = NO -+ -+# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) -+# and project file content. -+# This tag requires that the tag GENERATE_HTMLHELP is set to YES. -+ -+CHM_INDEX_ENCODING = -+ -+# The BINARY_TOC flag controls whether a binary table of contents is generated -+# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it -+# enables the Previous and Next buttons. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_HTMLHELP is set to YES. -+ -+BINARY_TOC = NO -+ -+# The TOC_EXPAND flag can be set to YES to add extra items for group members to -+# the table of contents of the HTML help documentation and to the tree view. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_HTMLHELP is set to YES. -+ -+TOC_EXPAND = NO -+ -+# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and -+# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that -+# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help -+# (.qch) of the generated HTML documentation. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+GENERATE_QHP = NO -+ -+# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify -+# the file name of the resulting .qch file. The path specified is relative to -+# the HTML output folder. -+# This tag requires that the tag GENERATE_QHP is set to YES. -+ -+QCH_FILE = -+ -+# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help -+# Project output. For more information please see Qt Help Project / Namespace -+# (see: -+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). -+# The default value is: org.doxygen.Project. -+# This tag requires that the tag GENERATE_QHP is set to YES. -+ -+QHP_NAMESPACE = -+ -+# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt -+# Help Project output. For more information please see Qt Help Project / Virtual -+# Folders (see: -+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders). -+# The default value is: doc. -+# This tag requires that the tag GENERATE_QHP is set to YES. -+ -+QHP_VIRTUAL_FOLDER = doc -+ -+# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom -+# filter to add. For more information please see Qt Help Project / Custom -+# Filters (see: -+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). -+# This tag requires that the tag GENERATE_QHP is set to YES. -+ -+QHP_CUST_FILTER_NAME = -+ -+# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the -+# custom filter to add. For more information please see Qt Help Project / Custom -+# Filters (see: -+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). -+# This tag requires that the tag GENERATE_QHP is set to YES. -+ -+QHP_CUST_FILTER_ATTRS = -+ -+# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this -+# project's filter section matches. Qt Help Project / Filter Attributes (see: -+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes). -+# This tag requires that the tag GENERATE_QHP is set to YES. -+ -+QHP_SECT_FILTER_ATTRS = -+ -+# The QHG_LOCATION tag can be used to specify the location (absolute path -+# including file name) of Qt's qhelpgenerator. If non-empty doxygen will try to -+# run qhelpgenerator on the generated .qhp file. -+# This tag requires that the tag GENERATE_QHP is set to YES. -+ -+QHG_LOCATION = -+ -+# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be -+# generated, together with the HTML files, they form an Eclipse help plugin. To -+# install this plugin and make it available under the help contents menu in -+# Eclipse, the contents of the directory containing the HTML and XML files needs -+# to be copied into the plugins directory of eclipse. The name of the directory -+# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. -+# After copying Eclipse needs to be restarted before the help appears. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+GENERATE_ECLIPSEHELP = NO -+ -+# A unique identifier for the Eclipse help plugin. When installing the plugin -+# the directory name containing the HTML and XML files should also have this -+# name. Each documentation set should have its own identifier. -+# The default value is: org.doxygen.Project. -+# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. -+ -+ECLIPSE_DOC_ID = org.doxygen.Project -+ -+# If you want full control over the layout of the generated HTML pages it might -+# be necessary to disable the index and replace it with your own. The -+# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top -+# of each HTML page. A value of NO enables the index and the value YES disables -+# it. Since the tabs in the index contain the same information as the navigation -+# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+DISABLE_INDEX = NO -+ -+# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index -+# structure should be generated to display hierarchical information. If the tag -+# value is set to YES, a side panel will be generated containing a tree-like -+# index structure (just like the one that is generated for HTML Help). For this -+# to work a browser that supports JavaScript, DHTML, CSS and frames is required -+# (i.e. any modern browser). Windows users are probably better off using the -+# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can -+# further fine-tune the look of the index. As an example, the default style -+# sheet generated by doxygen has an example that shows how to put an image at -+# the root of the tree instead of the PROJECT_NAME. Since the tree basically has -+# the same information as the tab index, you could consider setting -+# DISABLE_INDEX to YES when enabling this option. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+GENERATE_TREEVIEW = YES -+ -+# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that -+# doxygen will group on one line in the generated HTML documentation. -+# -+# Note that a value of 0 will completely suppress the enum values from appearing -+# in the overview section. -+# Minimum value: 0, maximum value: 20, default value: 4. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+ENUM_VALUES_PER_LINE = 4 -+ -+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used -+# to set the initial width (in pixels) of the frame in which the tree is shown. -+# Minimum value: 0, maximum value: 1500, default value: 250. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+TREEVIEW_WIDTH = 180 -+ -+# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to -+# external symbols imported via tag files in a separate window. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+EXT_LINKS_IN_WINDOW = NO -+ -+# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg -+# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see -+# https://inkscape.org) to generate formulas as SVG images instead of PNGs for -+# the HTML output. These images will generally look nicer at scaled resolutions. -+# Possible values are: png (the default) and svg (looks nicer but requires the -+# pdf2svg or inkscape tool). -+# The default value is: png. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+HTML_FORMULA_FORMAT = png -+ -+# Use this tag to change the font size of LaTeX formulas included as images in -+# the HTML documentation. When you change the font size after a successful -+# doxygen run you need to manually remove any form_*.png images from the HTML -+# output directory to force them to be regenerated. -+# Minimum value: 8, maximum value: 50, default value: 10. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+FORMULA_FONTSIZE = 10 -+ -+# Use the FORMULA_TRANSPARENT tag to determine whether or not the images -+# generated for formulas are transparent PNGs. Transparent PNGs are not -+# supported properly for IE 6.0, but are supported on all modern browsers. -+# -+# Note that when changing this option you need to delete any form_*.png files in -+# the HTML output directory before the changes have effect. -+# The default value is: YES. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+FORMULA_TRANSPARENT = YES -+ -+# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands -+# to create new LaTeX commands to be used in formulas as building blocks. See -+# the section "Including formulas" for details. -+ -+FORMULA_MACROFILE = -+ -+# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see -+# https://www.mathjax.org) which uses client side JavaScript for the rendering -+# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX -+# installed or if you want to formulas look prettier in the HTML output. When -+# enabled you may also need to install MathJax separately and configure the path -+# to it using the MATHJAX_RELPATH option. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+USE_MATHJAX = NO -+ -+# When MathJax is enabled you can set the default output format to be used for -+# the MathJax output. See the MathJax site (see: -+# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. -+# Possible values are: HTML-CSS (which is slower, but has the best -+# compatibility), NativeMML (i.e. MathML) and SVG. -+# The default value is: HTML-CSS. -+# This tag requires that the tag USE_MATHJAX is set to YES. -+ -+MATHJAX_FORMAT = HTML-CSS -+ -+# When MathJax is enabled you need to specify the location relative to the HTML -+# output directory using the MATHJAX_RELPATH option. The destination directory -+# should contain the MathJax.js script. For instance, if the mathjax directory -+# is located at the same level as the HTML output directory, then -+# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax -+# Content Delivery Network so you can quickly see the result without installing -+# MathJax. However, it is strongly recommended to install a local copy of -+# MathJax from https://www.mathjax.org before deployment. -+# The default value is: https://cdn.jsdelivr.net/npm/mathjax@2. -+# This tag requires that the tag USE_MATHJAX is set to YES. -+ -+MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest -+ -+# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax -+# extension names that should be enabled during MathJax rendering. For example -+# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols -+# This tag requires that the tag USE_MATHJAX is set to YES. -+ -+MATHJAX_EXTENSIONS = -+ -+# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces -+# of code that will be used on startup of the MathJax code. See the MathJax site -+# (see: -+# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an -+# example see the documentation. -+# This tag requires that the tag USE_MATHJAX is set to YES. -+ -+MATHJAX_CODEFILE = -+ -+# When the SEARCHENGINE tag is enabled doxygen will generate a search box for -+# the HTML output. The underlying search engine uses javascript and DHTML and -+# should work on any modern browser. Note that when using HTML help -+# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) -+# there is already a search function so this one should typically be disabled. -+# For large projects the javascript based search engine can be slow, then -+# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to -+# search using the keyboard; to jump to the search box use <access key> + S -+# (what the <access key> is depends on the OS and browser, but it is typically -+# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down -+# key> to jump into the search results window, the results can be navigated -+# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel -+# the search. The filter options can be selected when the cursor is inside the -+# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys> -+# to select a filter and <Enter> or <escape> to activate or cancel the filter -+# option. -+# The default value is: YES. -+# This tag requires that the tag GENERATE_HTML is set to YES. -+ -+SEARCHENGINE = NO -+ -+# When the SERVER_BASED_SEARCH tag is enabled the search engine will be -+# implemented using a web server instead of a web client using JavaScript. There -+# are two flavors of web server based searching depending on the EXTERNAL_SEARCH -+# setting. When disabled, doxygen will generate a PHP script for searching and -+# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing -+# and searching needs to be provided by external tools. See the section -+# "External Indexing and Searching" for details. -+# The default value is: NO. -+# This tag requires that the tag SEARCHENGINE is set to YES. -+ -+SERVER_BASED_SEARCH = NO -+ -+# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP -+# script for searching. Instead the search results are written to an XML file -+# which needs to be processed by an external indexer. Doxygen will invoke an -+# external search engine pointed to by the SEARCHENGINE_URL option to obtain the -+# search results. -+# -+# Doxygen ships with an example indexer (doxyindexer) and search engine -+# (doxysearch.cgi) which are based on the open source search engine library -+# Xapian (see: -+# https://xapian.org/). -+# -+# See the section "External Indexing and Searching" for details. -+# The default value is: NO. -+# This tag requires that the tag SEARCHENGINE is set to YES. -+ -+EXTERNAL_SEARCH = NO -+ -+# The SEARCHENGINE_URL should point to a search engine hosted by a web server -+# which will return the search results when EXTERNAL_SEARCH is enabled. -+# -+# Doxygen ships with an example indexer (doxyindexer) and search engine -+# (doxysearch.cgi) which are based on the open source search engine library -+# Xapian (see: -+# https://xapian.org/). See the section "External Indexing and Searching" for -+# details. -+# This tag requires that the tag SEARCHENGINE is set to YES. -+ -+SEARCHENGINE_URL = -+ -+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed -+# search data is written to a file for indexing by an external tool. With the -+# SEARCHDATA_FILE tag the name of this file can be specified. -+# The default file is: searchdata.xml. -+# This tag requires that the tag SEARCHENGINE is set to YES. -+ -+SEARCHDATA_FILE = searchdata.xml -+ -+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the -+# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is -+# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple -+# projects and redirect the results back to the right project. -+# This tag requires that the tag SEARCHENGINE is set to YES. -+ -+EXTERNAL_SEARCH_ID = -+ -+# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen -+# projects other than the one defined by this configuration file, but that are -+# all added to the same external search index. Each project needs to have a -+# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of -+# to a relative location where the documentation can be found. The format is: -+# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ... -+# This tag requires that the tag SEARCHENGINE is set to YES. -+ -+EXTRA_SEARCH_MAPPINGS = -+ -+#--------------------------------------------------------------------------- -+# Configuration options related to the LaTeX output -+#--------------------------------------------------------------------------- -+ -+# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output. -+# The default value is: YES. -+ -+GENERATE_LATEX = NO -+ -+# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a -+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of -+# it. -+# The default directory is: latex. -+# This tag requires that the tag GENERATE_LATEX is set to YES. -+ -+LATEX_OUTPUT = latex -+ -+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be -+# invoked. -+# -+# Note that when not enabling USE_PDFLATEX the default is latex when enabling -+# USE_PDFLATEX the default is pdflatex and when in the later case latex is -+# chosen this is overwritten by pdflatex. For specific output languages the -+# default can have been set differently, this depends on the implementation of -+# the output language. -+# This tag requires that the tag GENERATE_LATEX is set to YES. -+ -+LATEX_CMD_NAME = latex -+ -+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate -+# index for LaTeX. -+# Note: This tag is used in the Makefile / make.bat. -+# See also: LATEX_MAKEINDEX_CMD for the part in the generated output file -+# (.tex). -+# The default file is: makeindex. -+# This tag requires that the tag GENERATE_LATEX is set to YES. -+ -+MAKEINDEX_CMD_NAME = makeindex -+ -+# The LATEX_MAKEINDEX_CMD tag can be used to specify the command name to -+# generate index for LaTeX. In case there is no backslash (\) as first character -+# it will be automatically added in the LaTeX code. -+# Note: This tag is used in the generated output file (.tex). -+# See also: MAKEINDEX_CMD_NAME for the part in the Makefile / make.bat. -+# The default value is: makeindex. -+# This tag requires that the tag GENERATE_LATEX is set to YES. -+ -+LATEX_MAKEINDEX_CMD = makeindex -+ -+# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX -+# documents. This may be useful for small projects and may help to save some -+# trees in general. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_LATEX is set to YES. -+ -+COMPACT_LATEX = NO -+ -+# The PAPER_TYPE tag can be used to set the paper type that is used by the -+# printer. -+# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x -+# 14 inches) and executive (7.25 x 10.5 inches). -+# The default value is: a4. -+# This tag requires that the tag GENERATE_LATEX is set to YES. -+ -+PAPER_TYPE = a4 -+ -+# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names -+# that should be included in the LaTeX output. The package can be specified just -+# by its name or with the correct syntax as to be used with the LaTeX -+# \usepackage command. To get the times font for instance you can specify : -+# EXTRA_PACKAGES=times or EXTRA_PACKAGES={times} -+# To use the option intlimits with the amsmath package you can specify: -+# EXTRA_PACKAGES=[intlimits]{amsmath} -+# If left blank no extra packages will be included. -+# This tag requires that the tag GENERATE_LATEX is set to YES. -+ -+EXTRA_PACKAGES = -+ -+# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the -+# generated LaTeX document. The header should contain everything until the first -+# chapter. If it is left blank doxygen will generate a standard header. See -+# section "Doxygen usage" for information on how to let doxygen write the -+# default header to a separate file. -+# -+# Note: Only use a user-defined header if you know what you are doing! The -+# following commands have a special meaning inside the header: $title, -+# $datetime, $date, $doxygenversion, $projectname, $projectnumber, -+# $projectbrief, $projectlogo. Doxygen will replace $title with the empty -+# string, for the replacement values of the other commands the user is referred -+# to HTML_HEADER. -+# This tag requires that the tag GENERATE_LATEX is set to YES. -+ -+LATEX_HEADER = -+ -+# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the -+# generated LaTeX document. The footer should contain everything after the last -+# chapter. If it is left blank doxygen will generate a standard footer. See -+# LATEX_HEADER for more information on how to generate a default footer and what -+# special commands can be used inside the footer. -+# -+# Note: Only use a user-defined footer if you know what you are doing! -+# This tag requires that the tag GENERATE_LATEX is set to YES. -+ -+LATEX_FOOTER = -+ -+# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined -+# LaTeX style sheets that are included after the standard style sheets created -+# by doxygen. Using this option one can overrule certain style aspects. Doxygen -+# will copy the style sheet files to the output directory. -+# Note: The order of the extra style sheet files is of importance (e.g. the last -+# style sheet in the list overrules the setting of the previous ones in the -+# list). -+# This tag requires that the tag GENERATE_LATEX is set to YES. -+ -+LATEX_EXTRA_STYLESHEET = -+ -+# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or -+# other source files which should be copied to the LATEX_OUTPUT output -+# directory. Note that the files will be copied as-is; there are no commands or -+# markers available. -+# This tag requires that the tag GENERATE_LATEX is set to YES. -+ -+LATEX_EXTRA_FILES = -+ -+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is -+# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will -+# contain links (just like the HTML output) instead of page references. This -+# makes the output suitable for online browsing using a PDF viewer. -+# The default value is: YES. -+# This tag requires that the tag GENERATE_LATEX is set to YES. -+ -+PDF_HYPERLINKS = NO -+ -+# If the USE_PDFLATEX tag is set to YES, doxygen will use the engine as -+# specified with LATEX_CMD_NAME to generate the PDF file directly from the LaTeX -+# files. Set this option to YES, to get a higher quality PDF documentation. -+# -+# See also section LATEX_CMD_NAME for selecting the engine. -+# The default value is: YES. -+# This tag requires that the tag GENERATE_LATEX is set to YES. -+ -+USE_PDFLATEX = NO -+ -+# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode -+# command to the generated LaTeX files. This will instruct LaTeX to keep running -+# if errors occur, instead of asking the user for help. This option is also used -+# when generating formulas in HTML. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_LATEX is set to YES. -+ -+LATEX_BATCHMODE = NO -+ -+# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the -+# index chapters (such as File Index, Compound Index, etc.) in the output. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_LATEX is set to YES. -+ -+LATEX_HIDE_INDICES = NO -+ -+# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source -+# code with syntax highlighting in the LaTeX output. -+# -+# Note that which sources are shown also depends on other settings such as -+# SOURCE_BROWSER. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_LATEX is set to YES. -+ -+LATEX_SOURCE_CODE = NO -+ -+# The LATEX_BIB_STYLE tag can be used to specify the style to use for the -+# bibliography, e.g. plainnat, or ieeetr. See -+# https://en.wikipedia.org/wiki/BibTeX and \cite for more info. -+# The default value is: plain. -+# This tag requires that the tag GENERATE_LATEX is set to YES. -+ -+LATEX_BIB_STYLE = plain -+ -+# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated -+# page will contain the date and time when the page was generated. Setting this -+# to NO can help when comparing the output of multiple runs. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_LATEX is set to YES. -+ -+LATEX_TIMESTAMP = NO -+ -+# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute) -+# path from which the emoji images will be read. If a relative path is entered, -+# it will be relative to the LATEX_OUTPUT directory. If left blank the -+# LATEX_OUTPUT directory will be used. -+# This tag requires that the tag GENERATE_LATEX is set to YES. -+ -+LATEX_EMOJI_DIRECTORY = -+ -+#--------------------------------------------------------------------------- -+# Configuration options related to the RTF output -+#--------------------------------------------------------------------------- -+ -+# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The -+# RTF output is optimized for Word 97 and may not look too pretty with other RTF -+# readers/editors. -+# The default value is: NO. -+ -+GENERATE_RTF = NO -+ -+# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a -+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of -+# it. -+# The default directory is: rtf. -+# This tag requires that the tag GENERATE_RTF is set to YES. -+ -+RTF_OUTPUT = rtf -+ -+# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF -+# documents. This may be useful for small projects and may help to save some -+# trees in general. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_RTF is set to YES. -+ -+COMPACT_RTF = NO -+ -+# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will -+# contain hyperlink fields. The RTF file will contain links (just like the HTML -+# output) instead of page references. This makes the output suitable for online -+# browsing using Word or some other Word compatible readers that support those -+# fields. -+# -+# Note: WordPad (write) and others do not support links. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_RTF is set to YES. -+ -+RTF_HYPERLINKS = NO -+ -+# Load stylesheet definitions from file. Syntax is similar to doxygen's -+# configuration file, i.e. a series of assignments. You only have to provide -+# replacements, missing definitions are set to their default value. -+# -+# See also section "Doxygen usage" for information on how to generate the -+# default style sheet that doxygen normally uses. -+# This tag requires that the tag GENERATE_RTF is set to YES. -+ -+RTF_STYLESHEET_FILE = -+ -+# Set optional variables used in the generation of an RTF document. Syntax is -+# similar to doxygen's configuration file. A template extensions file can be -+# generated using doxygen -e rtf extensionFile. -+# This tag requires that the tag GENERATE_RTF is set to YES. -+ -+RTF_EXTENSIONS_FILE = -+ -+# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code -+# with syntax highlighting in the RTF output. -+# -+# Note that which sources are shown also depends on other settings such as -+# SOURCE_BROWSER. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_RTF is set to YES. -+ -+RTF_SOURCE_CODE = NO -+ -+#--------------------------------------------------------------------------- -+# Configuration options related to the man page output -+#--------------------------------------------------------------------------- -+ -+# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for -+# classes and files. -+# The default value is: NO. -+ -+GENERATE_MAN = NO -+ -+# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a -+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of -+# it. A directory man3 will be created inside the directory specified by -+# MAN_OUTPUT. -+# The default directory is: man. -+# This tag requires that the tag GENERATE_MAN is set to YES. -+ -+MAN_OUTPUT = man -+ -+# The MAN_EXTENSION tag determines the extension that is added to the generated -+# man pages. In case the manual section does not start with a number, the number -+# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is -+# optional. -+# The default value is: .3. -+# This tag requires that the tag GENERATE_MAN is set to YES. -+ -+MAN_EXTENSION = .3 -+ -+# The MAN_SUBDIR tag determines the name of the directory created within -+# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by -+# MAN_EXTENSION with the initial . removed. -+# This tag requires that the tag GENERATE_MAN is set to YES. -+ -+MAN_SUBDIR = -+ -+# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it -+# will generate one additional man file for each entity documented in the real -+# man page(s). These additional files only source the real man page, but without -+# them the man command would be unable to find the correct page. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_MAN is set to YES. -+ -+MAN_LINKS = NO -+ -+#--------------------------------------------------------------------------- -+# Configuration options related to the XML output -+#--------------------------------------------------------------------------- -+ -+# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that -+# captures the structure of the code including all documentation. -+# The default value is: NO. -+ -+GENERATE_XML = NO -+ -+# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a -+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of -+# it. -+# The default directory is: xml. -+# This tag requires that the tag GENERATE_XML is set to YES. -+ -+XML_OUTPUT = xml -+ -+# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program -+# listings (including syntax highlighting and cross-referencing information) to -+# the XML output. Note that enabling this will significantly increase the size -+# of the XML output. -+# The default value is: YES. -+# This tag requires that the tag GENERATE_XML is set to YES. -+ -+XML_PROGRAMLISTING = NO -+ -+# If the XML_NS_MEMB_FILE_SCOPE tag is set to YES, doxygen will include -+# namespace members in file scope as well, matching the HTML output. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_XML is set to YES. -+ -+XML_NS_MEMB_FILE_SCOPE = NO -+ -+#--------------------------------------------------------------------------- -+# Configuration options related to the DOCBOOK output -+#--------------------------------------------------------------------------- -+ -+# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files -+# that can be used to generate PDF. -+# The default value is: NO. -+ -+GENERATE_DOCBOOK = NO -+ -+# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put. -+# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in -+# front of it. -+# The default directory is: docbook. -+# This tag requires that the tag GENERATE_DOCBOOK is set to YES. -+ -+DOCBOOK_OUTPUT = docbook -+ -+# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the -+# program listings (including syntax highlighting and cross-referencing -+# information) to the DOCBOOK output. Note that enabling this will significantly -+# increase the size of the DOCBOOK output. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_DOCBOOK is set to YES. -+ -+DOCBOOK_PROGRAMLISTING = NO -+ -+#--------------------------------------------------------------------------- -+# Configuration options for the AutoGen Definitions output -+#--------------------------------------------------------------------------- -+ -+# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an -+# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures -+# the structure of the code including all documentation. Note that this feature -+# is still experimental and incomplete at the moment. -+# The default value is: NO. -+ -+GENERATE_AUTOGEN_DEF = NO -+ -+#--------------------------------------------------------------------------- -+# Configuration options related to the Perl module output -+#--------------------------------------------------------------------------- -+ -+# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module -+# file that captures the structure of the code including all documentation. -+# -+# Note that this feature is still experimental and incomplete at the moment. -+# The default value is: NO. -+ -+GENERATE_PERLMOD = NO -+ -+# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary -+# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI -+# output from the Perl module output. -+# The default value is: NO. -+# This tag requires that the tag GENERATE_PERLMOD is set to YES. -+ -+PERLMOD_LATEX = NO -+ -+# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely -+# formatted so it can be parsed by a human reader. This is useful if you want to -+# understand what is going on. On the other hand, if this tag is set to NO, the -+# size of the Perl module output will be much smaller and Perl will parse it -+# just the same. -+# The default value is: YES. -+# This tag requires that the tag GENERATE_PERLMOD is set to YES. -+ -+PERLMOD_PRETTY = YES -+ -+# The names of the make variables in the generated doxyrules.make file are -+# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful -+# so different doxyrules.make files included by the same Makefile don't -+# overwrite each other's variables. -+# This tag requires that the tag GENERATE_PERLMOD is set to YES. -+ -+PERLMOD_MAKEVAR_PREFIX = -+ -+#--------------------------------------------------------------------------- -+# Configuration options related to the preprocessor -+#--------------------------------------------------------------------------- -+ -+# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all -+# C-preprocessor directives found in the sources and include files. -+# The default value is: YES. -+ -+ENABLE_PREPROCESSING = YES -+ -+# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names -+# in the source code. If set to NO, only conditional compilation will be -+# performed. Macro expansion can be done in a controlled way by setting -+# EXPAND_ONLY_PREDEF to YES. -+# The default value is: NO. -+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -+ -+MACRO_EXPANSION = YES -+ -+# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then -+# the macro expansion is limited to the macros specified with the PREDEFINED and -+# EXPAND_AS_DEFINED tags. -+# The default value is: NO. -+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -+ -+EXPAND_ONLY_PREDEF = NO -+ -+# If the SEARCH_INCLUDES tag is set to YES, the include files in the -+# INCLUDE_PATH will be searched if a #include is found. -+# The default value is: YES. -+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -+ -+SEARCH_INCLUDES = YES -+ -+# The INCLUDE_PATH tag can be used to specify one or more directories that -+# contain include files that are not input files but should be processed by the -+# preprocessor. -+# This tag requires that the tag SEARCH_INCLUDES is set to YES. -+ -+INCLUDE_PATH = -+ -+# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard -+# patterns (like *.h and *.hpp) to filter out the header-files in the -+# directories. If left blank, the patterns specified with FILE_PATTERNS will be -+# used. -+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -+ -+INCLUDE_FILE_PATTERNS = -+ -+# The PREDEFINED tag can be used to specify one or more macro names that are -+# defined before the preprocessor is started (similar to the -D option of e.g. -+# gcc). The argument of the tag is a list of macros of the form: name or -+# name=definition (no spaces). If the definition and the "=" are omitted, "=1" -+# is assumed. To prevent a macro definition from being undefined via #undef or -+# recursively expanded use the := operator instead of the = operator. -+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -+ -+PREDEFINED = -+ -+# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this -+# tag can be used to specify a list of macro names that should be expanded. The -+# macro definition that is found in the sources will be used. Use the PREDEFINED -+# tag if you want to use a different macro definition that overrules the -+# definition found in the source code. -+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -+ -+EXPAND_AS_DEFINED = -+ -+# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will -+# remove all references to function-like macros that are alone on a line, have -+# an all uppercase name, and do not end with a semicolon. Such function macros -+# are typically used for boiler-plate code, and will confuse the parser if not -+# removed. -+# The default value is: YES. -+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -+ -+SKIP_FUNCTION_MACROS = YES -+ -+#--------------------------------------------------------------------------- -+# Configuration options related to external references -+#--------------------------------------------------------------------------- -+ -+# The TAGFILES tag can be used to specify one or more tag files. For each tag -+# file the location of the external documentation should be added. The format of -+# a tag file without this location is as follows: -+# TAGFILES = file1 file2 ... -+# Adding location for the tag files is done as follows: -+# TAGFILES = file1=loc1 "file2 = loc2" ... -+# where loc1 and loc2 can be relative or absolute paths or URLs. See the -+# section "Linking to external documentation" for more information about the use -+# of tag files. -+# Note: Each tag file must have a unique name (where the name does NOT include -+# the path). If a tag file is not located in the directory in which doxygen is -+# run, you must also specify the path to the tagfile here. -+ -+TAGFILES = -+ -+# When a file name is specified after GENERATE_TAGFILE, doxygen will create a -+# tag file that is based on the input files it reads. See section "Linking to -+# external documentation" for more information about the usage of tag files. -+ -+GENERATE_TAGFILE = -+ -+# If the ALLEXTERNALS tag is set to YES, all external class will be listed in -+# the class index. If set to NO, only the inherited external classes will be -+# listed. -+# The default value is: NO. -+ -+ALLEXTERNALS = NO -+ -+# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed -+# in the modules index. If set to NO, only the current project's groups will be -+# listed. -+# The default value is: YES. -+ -+EXTERNAL_GROUPS = YES -+ -+# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in -+# the related pages index. If set to NO, only the current project's pages will -+# be listed. -+# The default value is: YES. -+ -+EXTERNAL_PAGES = YES -+ -+#--------------------------------------------------------------------------- -+# Configuration options related to the dot tool -+#--------------------------------------------------------------------------- -+ -+# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram -+# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to -+# NO turns the diagrams off. Note that this option also works with HAVE_DOT -+# disabled, but it is recommended to install and use dot, since it yields more -+# powerful graphs. -+# The default value is: YES. -+ -+CLASS_DIAGRAMS = YES -+ -+# You can include diagrams made with dia in doxygen documentation. Doxygen will -+# then run dia to produce the diagram and insert it in the documentation. The -+# DIA_PATH tag allows you to specify the directory where the dia binary resides. -+# If left empty dia is assumed to be found in the default search path. -+ -+DIA_PATH = -+ -+# If set to YES the inheritance and collaboration graphs will hide inheritance -+# and usage relations if the target is undocumented or is not a class. -+# The default value is: YES. -+ -+HIDE_UNDOC_RELATIONS = YES -+ -+# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is -+# available from the path. This tool is part of Graphviz (see: -+# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent -+# Bell Labs. The other options in this section have no effect if this option is -+# set to NO -+# The default value is: NO. -+ -+HAVE_DOT = YES -+ -+# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed -+# to run in parallel. When set to 0 doxygen will base this on the number of -+# processors available in the system. You can set it explicitly to a value -+# larger than 0 to get control over the balance between CPU load and processing -+# speed. -+# Minimum value: 0, maximum value: 32, default value: 0. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+DOT_NUM_THREADS = 0 -+ -+# When you want a differently looking font in the dot files that doxygen -+# generates you can specify the font name using DOT_FONTNAME. You need to make -+# sure dot is able to find the font, which can be done by putting it in a -+# standard location or by setting the DOTFONTPATH environment variable or by -+# setting DOT_FONTPATH to the directory containing the font. -+# The default value is: Helvetica. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+DOT_FONTNAME = Helvetica -+ -+# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of -+# dot graphs. -+# Minimum value: 4, maximum value: 24, default value: 10. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+DOT_FONTSIZE = 10 -+ -+# By default doxygen will tell dot to use the default font as specified with -+# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set -+# the path where dot can find it using this tag. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+DOT_FONTPATH = -+ -+# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for -+# each documented class showing the direct and indirect inheritance relations. -+# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO. -+# The default value is: YES. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+CLASS_GRAPH = YES -+ -+# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a -+# graph for each documented class showing the direct and indirect implementation -+# dependencies (inheritance, containment, and class references variables) of the -+# class with other documented classes. -+# The default value is: YES. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+COLLABORATION_GRAPH = NO -+ -+# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for -+# groups, showing the direct groups dependencies. -+# The default value is: YES. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+GROUP_GRAPHS = YES -+ -+# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and -+# collaboration diagrams in a style similar to the OMG's Unified Modeling -+# Language. -+# The default value is: NO. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+UML_LOOK = NO -+ -+# If the UML_LOOK tag is enabled, the fields and methods are shown inside the -+# class node. If there are many fields or methods and many nodes the graph may -+# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the -+# number of items for each type to make the size more manageable. Set this to 0 -+# for no limit. Note that the threshold may be exceeded by 50% before the limit -+# is enforced. So when you set the threshold to 10, up to 15 fields may appear, -+# but if the number exceeds 15, the total amount of fields shown is limited to -+# 10. -+# Minimum value: 0, maximum value: 100, default value: 10. -+# This tag requires that the tag UML_LOOK is set to YES. -+ -+UML_LIMIT_NUM_FIELDS = 10 -+ -+# If the DOT_UML_DETAILS tag is set to NO, doxygen will show attributes and -+# methods without types and arguments in the UML graphs. If the DOT_UML_DETAILS -+# tag is set to YES, doxygen will add type and arguments for attributes and -+# methods in the UML graphs. If the DOT_UML_DETAILS tag is set to NONE, doxygen -+# will not generate fields with class member information in the UML graphs. The -+# class diagrams will look similar to the default class diagrams but using UML -+# notation for the relationships. -+# Possible values are: NO, YES and NONE. -+# The default value is: NO. -+# This tag requires that the tag UML_LOOK is set to YES. -+ -+DOT_UML_DETAILS = NO -+ -+# The DOT_WRAP_THRESHOLD tag can be used to set the maximum number of characters -+# to display on a single line. If the actual line length exceeds this threshold -+# significantly it will wrapped across multiple lines. Some heuristics are apply -+# to avoid ugly line breaks. -+# Minimum value: 0, maximum value: 1000, default value: 17. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+DOT_WRAP_THRESHOLD = 17 -+ -+# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and -+# collaboration graphs will show the relations between templates and their -+# instances. -+# The default value is: NO. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+TEMPLATE_RELATIONS = NO -+ -+# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to -+# YES then doxygen will generate a graph for each documented file showing the -+# direct and indirect include dependencies of the file with other documented -+# files. -+# The default value is: YES. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+INCLUDE_GRAPH = YES -+ -+# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are -+# set to YES then doxygen will generate a graph for each documented file showing -+# the direct and indirect include dependencies of the file with other documented -+# files. -+# The default value is: YES. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+INCLUDED_BY_GRAPH = YES -+ -+# If the CALL_GRAPH tag is set to YES then doxygen will generate a call -+# dependency graph for every global function or class method. -+# -+# Note that enabling this option will significantly increase the time of a run. -+# So in most cases it will be better to enable call graphs for selected -+# functions only using the \callgraph command. Disabling a call graph can be -+# accomplished by means of the command \hidecallgraph. -+# The default value is: NO. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+CALL_GRAPH = YES -+ -+# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller -+# dependency graph for every global function or class method. -+# -+# Note that enabling this option will significantly increase the time of a run. -+# So in most cases it will be better to enable caller graphs for selected -+# functions only using the \callergraph command. Disabling a caller graph can be -+# accomplished by means of the command \hidecallergraph. -+# The default value is: NO. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+CALLER_GRAPH = NO -+ -+# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical -+# hierarchy of all classes instead of a textual one. -+# The default value is: YES. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+GRAPHICAL_HIERARCHY = YES -+ -+# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the -+# dependencies a directory has on other directories in a graphical way. The -+# dependency relations are determined by the #include relations between the -+# files in the directories. -+# The default value is: YES. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+DIRECTORY_GRAPH = YES -+ -+# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images -+# generated by dot. For an explanation of the image formats see the section -+# output formats in the documentation of the dot tool (Graphviz (see: -+# http://www.graphviz.org/)). -+# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order -+# to make the SVG files visible in IE 9+ (other browsers do not have this -+# requirement). -+# Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo, -+# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and -+# png:gdiplus:gdiplus. -+# The default value is: png. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+DOT_IMAGE_FORMAT = png -+ -+# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to -+# enable generation of interactive SVG images that allow zooming and panning. -+# -+# Note that this requires a modern browser other than Internet Explorer. Tested -+# and working are Firefox, Chrome, Safari, and Opera. -+# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make -+# the SVG files visible. Older versions of IE do not have SVG support. -+# The default value is: NO. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+INTERACTIVE_SVG = NO -+ -+# The DOT_PATH tag can be used to specify the path where the dot tool can be -+# found. If left blank, it is assumed the dot tool can be found in the path. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+DOT_PATH = -+ -+# The DOTFILE_DIRS tag can be used to specify one or more directories that -+# contain dot files that are included in the documentation (see the \dotfile -+# command). -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+DOTFILE_DIRS = -+ -+# The MSCFILE_DIRS tag can be used to specify one or more directories that -+# contain msc files that are included in the documentation (see the \mscfile -+# command). -+ -+MSCFILE_DIRS = -+ -+# The DIAFILE_DIRS tag can be used to specify one or more directories that -+# contain dia files that are included in the documentation (see the \diafile -+# command). -+ -+DIAFILE_DIRS = -+ -+# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the -+# path where java can find the plantuml.jar file. If left blank, it is assumed -+# PlantUML is not used or called during a preprocessing step. Doxygen will -+# generate a warning when it encounters a \startuml command in this case and -+# will not generate output for the diagram. -+ -+PLANTUML_JAR_PATH = -+ -+# When using plantuml, the PLANTUML_CFG_FILE tag can be used to specify a -+# configuration file for plantuml. -+ -+PLANTUML_CFG_FILE = -+ -+# When using plantuml, the specified paths are searched for files specified by -+# the !include statement in a plantuml block. -+ -+PLANTUML_INCLUDE_PATH = -+ -+# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes -+# that will be shown in the graph. If the number of nodes in a graph becomes -+# larger than this value, doxygen will truncate the graph, which is visualized -+# by representing a node as a red box. Note that doxygen if the number of direct -+# children of the root node in a graph is already larger than -+# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that -+# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH. -+# Minimum value: 0, maximum value: 10000, default value: 50. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+DOT_GRAPH_MAX_NODES = 200 -+ -+# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs -+# generated by dot. A depth value of 3 means that only nodes reachable from the -+# root by following a path via at most 3 edges will be shown. Nodes that lay -+# further from the root node will be omitted. Note that setting this option to 1 -+# or 2 may greatly reduce the computation time needed for large code bases. Also -+# note that the size of a graph can be further restricted by -+# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction. -+# Minimum value: 0, maximum value: 1000, default value: 0. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+MAX_DOT_GRAPH_DEPTH = 0 -+ -+# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent -+# background. This is disabled by default, because dot on Windows does not seem -+# to support this out of the box. -+# -+# Warning: Depending on the platform used, enabling this option may lead to -+# badly anti-aliased labels on the edges of a graph (i.e. they become hard to -+# read). -+# The default value is: NO. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+DOT_TRANSPARENT = NO -+ -+# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output -+# files in one run (i.e. multiple -o and -T options on the command line). This -+# makes dot run faster, but since only newer versions of dot (>1.8.10) support -+# this, this feature is disabled by default. -+# The default value is: NO. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+DOT_MULTI_TARGETS = NO -+ -+# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page -+# explaining the meaning of the various boxes and arrows in the dot generated -+# graphs. -+# The default value is: YES. -+# This tag requires that the tag HAVE_DOT is set to YES. -+ -+GENERATE_LEGEND = YES -+ -+# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate -+# files that are used to generate the various graphs. -+# -+# Note: This setting is not only used for dot files but also for msc and -+# plantuml temporary files. -+# The default value is: YES. -+ -+DOT_CLEANUP = YES -diff --git a/src/hooks/dhcp/ping_check/Makefile.am b/src/hooks/dhcp/ping_check/Makefile.am -new file mode 100644 -index 0000000000..a7ea17f400 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/Makefile.am -@@ -0,0 +1,104 @@ -+SUBDIRS = . libloadtests tests -+ -+AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib -+AM_CPPFLAGS += $(BOOST_INCLUDES) $(CRYPTO_CFLAGS) $(CRYPTO_INCLUDES) -+AM_CXXFLAGS = $(KEA_CXXFLAGS) -+ -+# Ensure that the message file and doxygen file is included in the distribution -+EXTRA_DIST = ping_check_messages.mes -+ -+CLEANFILES = *.gcno *.gcda -+ -+# convenience archive -+ -+noinst_LTLIBRARIES = libping_check.la -+ -+libping_check_la_SOURCES = ping_check_callouts.cc -+libping_check_la_SOURCES += ping_check_log.cc ping_check_log.h -+libping_check_la_SOURCES += ping_check_messages.cc ping_check_messages.h -+libping_check_la_SOURCES += icmp_endpoint.h icmp_socket.h -+libping_check_la_SOURCES += ping_context.cc ping_context.h -+libping_check_la_SOURCES += ping_context_store.cc ping_context_store.h -+libping_check_la_SOURCES += icmp_msg.h icmp_msg.cc -+libping_check_la_SOURCES += ping_channel.cc ping_channel.h -+libping_check_la_SOURCES += ping_check_mgr.cc ping_check_mgr.h -+libping_check_la_SOURCES += ping_check_config.cc ping_check_config.h -+libping_check_la_SOURCES += config_cache.cc config_cache.h -+libping_check_la_SOURCES += version.cc -+ -+libping_check_la_CXXFLAGS = $(AM_CXXFLAGS) -+libping_check_la_CPPFLAGS = $(AM_CPPFLAGS) -+ -+# install the shared object into $(libdir)/kea/hooks -+lib_hooksdir = $(libdir)/kea/hooks -+lib_hooks_LTLIBRARIES = libdhcp_ping_check.la -+ -+libdhcp_ping_check_la_SOURCES = -+libdhcp_ping_check_la_LDFLAGS = $(AM_LDFLAGS) -+libdhcp_ping_check_la_LDFLAGS += -avoid-version -export-dynamic -module -+libdhcp_ping_check_la_LIBADD = libping_check.la -+libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la -+libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/process/libkea-process.la -+libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/eval/libkea-eval.la -+libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la -+libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/stats/libkea-stats.la -+libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la -+libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/http/libkea-http.la -+libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la -+libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la -+libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/database/libkea-database.la -+libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/cc/libkea-cc.la -+libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la -+libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/dns/libkea-dns++.la -+libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la -+libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/log/libkea-log.la -+libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/util/libkea-util.la -+libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la -+libdhcp_ping_check_la_LIBADD += $(LOG4CPLUS_LIBS) -+libdhcp_ping_check_la_LIBADD += $(CRYPTO_LIBS) -+libdhcp_ping_check_la_LIBADD += $(BOOST_LIBS) -+ -+# Doxygen documentation -+EXTRA_DIST += ping_check.dox Doxyfile -+ -+devel: -+ mkdir -p html -+ (cat Doxyfile; echo PROJECT_NUMBER=$(PACKAGE_VERSION)) | doxygen - > html/doxygen.log 2> html/doxygen-error.log -+ echo `grep -i ": warning:" html/doxygen-error.log | wc -l` warnings/errors detected. -+ -+clean-local: -+ rm -rf html -+ -+# If we want to get rid of all generated messages files, we need to use -+# make maintainer-clean. The proper way to introduce custom commands for -+# that operation is to define maintainer-clean-local target. However, -+# make maintainer-clean also removes Makefile, so running configure script -+# is required. To make it easy to rebuild messages without going through -+# reconfigure, a new target messages-clean has been added. -+maintainer-clean-local: -+ rm -f ping_check_messages.h ping_check_messages.cc -+ -+# To regenerate messages files, one can do: -+# -+# make messages-clean -+# make messages -+# -+# This is needed only when a .mes file is modified. -+messages-clean: maintainer-clean-local -+ -+if GENERATE_MESSAGES -+ -+# Define rule to build logging source files from message file -+messages: ping_check_messages.h ping_check_messages.cc -+ @echo Message files regenerated -+ -+ping_check_messages.h ping_check_messages.cc: ping_check_messages.mes -+ (cd $(top_srcdir); \ -+ $(abs_top_builddir)/src/lib/log/compiler/kea-msg-compiler src/hooks/dhcp/ping_check/ping_check_messages.mes) -+ -+else -+ -+messages ping_check_messages.h ping_check_messages.cc: -+ @echo Messages generation disabled. Configure with --enable-generate-messages to enable it. -+ -+endif -diff --git a/src/hooks/dhcp/ping_check/config_cache.cc b/src/hooks/dhcp/ping_check/config_cache.cc -new file mode 100644 -index 0000000000..9a8f9dd4bb ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/config_cache.cc -@@ -0,0 +1,107 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#include <config.h> -+ -+#include <config_cache.h> -+#include <util/multi_threading_mgr.h> -+ -+using namespace isc; -+using namespace isc::data; -+using namespace isc::dhcp; -+using namespace isc::util; -+using namespace std; -+ -+namespace isc { -+namespace ping_check { -+ -+PingCheckConfigPtr& -+ConfigCache::getGlobalConfig() { -+ return (global_config_); -+} -+ -+void -+ConfigCache::setGlobalConfig(PingCheckConfigPtr& config) { -+ if (!config) { -+ isc_throw(BadValue, "ConfigCache - global config cannot be empty"); -+ } -+ -+ global_config_ = config; -+} -+ -+bool -+ConfigCache::findConfig(const SubnetID& subnet_id, PingCheckConfigPtr& config) { -+ MultiThreadingLock lock(*mutex_); -+ return (findConfigInternal(subnet_id, config)); -+} -+ -+bool -+ConfigCache::findConfigInternal(const SubnetID& subnet_id, PingCheckConfigPtr& config) const { -+ auto it = configs_.find(subnet_id); -+ if (it != configs_.end()) { -+ config = it->second; -+ return (true); -+ } -+ -+ config = PingCheckConfigPtr(); -+ return (false); -+} -+ -+PingCheckConfigPtr -+ConfigCache::parseAndCacheConfig(const SubnetID& subnet_id, ConstElementPtr& user_context) { -+ PingCheckConfigPtr config; -+ if (user_context) { -+ ConstElementPtr ping_check_params = user_context->get("ping-check"); -+ if (ping_check_params) { -+ // Copy construct from global to start with. -+ config.reset(new PingCheckConfig(*getGlobalConfig())); -+ -+ // Now parse in subnet-specific values. This may throw a DhcpConfigError but -+ // that's OK, dealt with by the caller. -+ try { -+ config->parse(ping_check_params); -+ } catch (...) { -+ throw; -+ } -+ } -+ } -+ -+ // Cache the config. We allow empty configs so higher precedence scopes may -+ // override lower precedence scopes. -+ cacheConfig(subnet_id, config); -+ return (config); -+} -+ -+void -+ConfigCache::cacheConfig(const SubnetID& subnet_id, PingCheckConfigPtr& config) { -+ MultiThreadingLock lock(*mutex_); -+ configs_[subnet_id] = config; -+} -+ -+void -+ConfigCache::flush() { -+ MultiThreadingLock lock(*mutex_); -+ // Discard the contents. -+ configs_.clear(); -+ -+ // We use modification time to remember the last time we flushed. -+ updateModificationTime(); -+} -+ -+size_t -+ConfigCache::size() { -+ MultiThreadingLock lock(*mutex_); -+ return (configs_.size()); -+} -+ -+boost::posix_time::ptime -+ConfigCache::getLastFlushTime() { -+ MultiThreadingLock lock(*mutex_); -+ return (BaseStampedElement::getModificationTime()); -+} -+ -+} // end of namespace ping_check -+} // end of namespace isc -diff --git a/src/hooks/dhcp/ping_check/config_cache.h b/src/hooks/dhcp/ping_check/config_cache.h -new file mode 100644 -index 0000000000..b69cf6f124 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/config_cache.h -@@ -0,0 +1,146 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#ifndef CONFIG_CACHE_H -+#define CONFIG_CACHE_H -+ -+#include <ping_check_config.h> -+#include <cc/base_stamped_element.h> -+#include <cc/data.h> -+#include <dhcpsrv/subnet.h> -+ -+#include <map> -+#include <mutex> -+ -+namespace isc { -+namespace ping_check { -+ -+/// @brief ConfigCache stores ping check config per subnet -+/// -+/// The intent is parse subnet ping-check parameters from its user-context -+/// as few times as possible rather than on every ping check request, while -+/// also allowing for run time updates via config back end or subnet cmds. -+/// -+/// For every subnet we store: -+/// -+/// -# subnet id -+/// -# PingCheckConfig pointer -+/// where: -+/// - empty config pointer means that subnet does not specify ping check config -+/// - non-empty means subnet specifies at least some ping check parameters -+/// -+/// Each time we clear the cache we update the modification time. -+/// -+/// When presented with a subnet: -+/// -+/// 1. no cache entry: -+/// cache it -+/// -+/// 2. entry exists: -+/// subnet mod time >= last flush -+/// cache is stale flush it -+/// cache it -+/// -+/// subnet mod time < last flush -+/// use it -+/// -+class ConfigCache : public data::BaseStampedElement { -+public: -+ /// @brief Constructor -+ ConfigCache() : configs_(), global_config_(new PingCheckConfig()), mutex_(new std::mutex) { -+ } -+ -+ /// @brief Destructor -+ virtual ~ConfigCache() = default; -+ -+ /// @brief Get the config for a given subnet. -+ /// -+ /// @param subnet_id ID of the subnet for which the config is desired. -+ /// @param[out] config a reference to a pointer in which to store the -+ /// config if found. If there is no entry for the subnet, it will be set -+ /// to an empty pointer. -+ /// -+ /// @return True if an entry for subnet was found, false otherwise. This -+ /// allows callers to distinguish between unknown subnets (entries that do -+ /// not exist) and subnets that are known but do not define a config. -+ bool findConfig(const dhcp::SubnetID& subnet_id, -+ PingCheckConfigPtr& config); -+ -+ /// @brief Parses a config string and caches for the given subnet. -+ /// -+ /// @param subnet_id ID of the subnet for which the config is desired. -+ /// @param user_context user-context Element map of the subnet. -+ /// -+ /// @return pointer to the parsed config. -+ /// @throw BadValue if an error occurred during config parsing. -+ PingCheckConfigPtr parseAndCacheConfig(const dhcp::SubnetID& subnet_id, -+ data::ConstElementPtr& user_context); -+ -+ /// @brief Adds (or replaces) the config for a given subnet to the cache. -+ /// -+ /// @param subnet_id ID of the subnet for which the config is desired. -+ /// @param config pointer to the config to store. This may be an -+ /// empty pointer. -+ void cacheConfig(const dhcp::SubnetID& subnet_id, -+ PingCheckConfigPtr& config); -+ -+ /// @brief Discards the subnet entries in the cache. -+ void flush(); -+ -+ /// @brief Get the number of entries in the cache. -+ /// -+ /// @return number of entries in the cache. -+ size_t size(); -+ -+ /// @brief Get the last time the cache was flushed. -+ /// -+ /// @return the last time the cache was flushed (or the time it was -+ /// created if it has never been flushed). -+ boost::posix_time::ptime getLastFlushTime(); -+ -+ /// @brief Get the global level configuration. -+ /// -+ /// @return pointer to the global configuration. -+ PingCheckConfigPtr& getGlobalConfig(); -+ -+ /// @brief Set the global level configuration. -+ /// -+ /// @param config configuration to store as the global configuration. -+ void setGlobalConfig(PingCheckConfigPtr& config); -+ -+private: -+ /// @brief Get the config for a given subnet. -+ /// -+ /// Must be called from with a thread-safe context. -+ /// -+ /// @param subnet_id ID of the subnet for which the config is desired. -+ /// @param[out] config a reference to a pointer in which to store the -+ /// config if found. If there is no entry for the subnet, it will be set -+ /// to an empty pointer. -+ /// -+ /// @return True if an entry for subnet was found, false otherwise. This -+ /// allows callers to distinguish between unknown subnets (entries that do -+ /// not exist) and subnets that are known but do not define a config. -+ bool findConfigInternal(const dhcp::SubnetID& subnet_id, -+ PingCheckConfigPtr& config) const; -+ -+ /// @brief Per subnet config cache. Note that the global config in stored -+ /// using SUBNET_ID_GLOBAL. -+ std::map<dhcp::SubnetID, PingCheckConfigPtr> configs_; -+ -+ /// @brief Stores the global configuration parameters. -+ PingCheckConfigPtr global_config_; -+ -+ /// @brief The mutex used to protect internal state. -+ const boost::scoped_ptr<std::mutex> mutex_; -+}; -+ -+/// @brief Defines a shared pointer to a ConfigCache. -+typedef boost::shared_ptr<ConfigCache> ConfigCachePtr; -+ -+} // end of namespace ping_check -+} // end of namespace isc -+#endif -diff --git a/src/hooks/dhcp/ping_check/icmp_endpoint.h b/src/hooks/dhcp/ping_check/icmp_endpoint.h -new file mode 100644 -index 0000000000..5d047d286f ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/icmp_endpoint.h -@@ -0,0 +1,134 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#ifndef ICMP_ENDPOINT_H -+#define ICMP_ENDPOINT_H 1 -+ -+#include <asiolink/io_endpoint.h> -+ -+namespace isc { -+namespace ping_check { -+ -+/// @brief The @c ICMPEndpoint class is a concrete derived class of -+/// @c IOEndpoint that represents an endpoint of a ICMP packet. -+/// -+/// Other notes about @c TCPEndpoint applies to this class, too. -+class ICMPEndpoint : public asiolink::IOEndpoint { -+public: -+ /// -+ /// @name Constructors and Destructor. -+ /// -+ //@{ -+ -+ /// @brief Default Constructor -+ /// -+ /// Creates an internal endpoint. This is expected to be set by some -+ /// external call. -+ ICMPEndpoint() : -+ asio_endpoint_placeholder_(new boost::asio::ip::icmp::endpoint()), -+ asio_endpoint_(*asio_endpoint_placeholder_) -+ {} -+ -+ /// @brief Constructor from an address. -+ /// -+ /// @param address The IP address of the endpoint. -+ explicit ICMPEndpoint(const asiolink::IOAddress& address) : -+ asio_endpoint_placeholder_( -+ new boost::asio::ip::icmp::endpoint(boost::asio::ip::make_address(address.toText()), 0)), -+ asio_endpoint_(*asio_endpoint_placeholder_) -+ {} -+ -+ /// @brief Copy Constructor from an ASIO ICMP endpoint. -+ /// -+ /// This constructor is designed to be an efficient wrapper for the -+ /// corresponding ASIO class, @c icmp::endpoint. -+ /// -+ /// @param asio_endpoint The ASIO representation of the ICMP endpoint. -+ explicit ICMPEndpoint(boost::asio::ip::icmp::endpoint& asio_endpoint) : -+ asio_endpoint_placeholder_(0), asio_endpoint_(asio_endpoint) -+ {} -+ -+ /// @brief Constructor from a const ASIO ICMP endpoint. -+ /// -+ /// This constructor is designed to be an efficient wrapper for the -+ /// corresponding ASIO class, @c icmp::endpoint. -+ /// -+ /// @param asio_endpoint The ASIO representation of the TCP endpoint. -+ explicit ICMPEndpoint(const boost::asio::ip::icmp::endpoint& asio_endpoint) : -+ asio_endpoint_placeholder_(new boost::asio::ip::icmp::endpoint(asio_endpoint)), -+ asio_endpoint_(*asio_endpoint_placeholder_) -+ {} -+ -+ /// @brief The destructor. -+ virtual ~ICMPEndpoint() { delete asio_endpoint_placeholder_; } -+ //@} -+ -+ /// @brief Fetches the IP address of the endpoint. -+ /// -+ /// @return the endpoint's IP address as an IOAddress. -+ virtual asiolink::IOAddress getAddress() const { -+ return (asio_endpoint_.address()); -+ } -+ -+ /// @brief Fetches the IP address of the endpoint in native form. -+ /// -+ /// @return the endpoint's IP address as a struct sockaddr. -+ virtual const struct sockaddr& getSockAddr() const { -+ return (*asio_endpoint_.data()); -+ } -+ -+ /// @brief Fetches the IP port number of the endpoint. -+ /// -+ /// @return the endpoint's port number as a unit16_t. -+ virtual uint16_t getPort() const { -+ return (asio_endpoint_.port()); -+ } -+ -+ /// @brief Fetches the network protocol of the endpoint. -+ /// -+ /// @return the endpoint's protocol as a short -+ virtual short getProtocol() const { -+ return (asio_endpoint_.protocol().protocol()); -+ } -+ -+ /// @brief Fetches the network protocol family of the endpoint. -+ /// -+ /// @return the endpoint's protocol as a short -+ virtual short getFamily() const { -+ return (asio_endpoint_.protocol().family()); -+ } -+ -+ /// @brief Fetches the underlying ASIO endpoint implementation -+ /// -+ /// This is not part of the exposed IOEndpoint API but allows -+ /// direct access to the ASIO implementation of the endpoint -+ /// -+ /// @return the wrapped ASIO endpoint instance as a const -+ inline const boost::asio::ip::icmp::endpoint& getASIOEndpoint() const { -+ return (asio_endpoint_); -+ } -+ -+ /// @brief Fetches the underlying ASIO endpoint implementation -+ /// -+ /// This is not part of the exposed IOEndpoint API but allows -+ /// direct access to the ASIO implementation of the endpoint -+ /// -+ /// @return the wrapped ASIO endpoint instance as a non-const -+ inline boost::asio::ip::icmp::endpoint& getASIOEndpoint() { -+ return (asio_endpoint_); -+ } -+ -+private: -+ /// @brief Pointer to the ASIO endpoint placeholder. -+ boost::asio::ip::icmp::endpoint* asio_endpoint_placeholder_; -+ -+ /// @brief Reference to the underlying ASIO endpoint instance. -+ boost::asio::ip::icmp::endpoint& asio_endpoint_; -+}; -+ -+} // namespace ping_check -+} // namespace isc -+#endif // ICMP_ENDPOINT_H -diff --git a/src/hooks/dhcp/ping_check/icmp_msg.cc b/src/hooks/dhcp/ping_check/icmp_msg.cc -new file mode 100644 -index 0000000000..3d236820da ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/icmp_msg.cc -@@ -0,0 +1,112 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#include <config.h> -+#include <icmp_msg.h> -+#include <util/io.h> -+#include <exceptions/exceptions.h> -+ -+#include <netinet/ip_icmp.h> -+#include <iostream> -+ -+using namespace isc; -+using namespace isc::asiolink; -+using namespace isc::util; -+ -+namespace isc { -+namespace ping_check { -+ -+ICMPMsg::ICMPMsg() -+ : source_(IOAddress::IPV4_ZERO_ADDRESS()), -+ destination_(IOAddress::IPV4_ZERO_ADDRESS()), -+ msg_type_(0), code_(0), check_sum_(0), id_(0), sequence_(0), -+ payload_(0) { -+} -+ -+ICMPMsgPtr -+ICMPMsg::unpack(const uint8_t* wire_data, size_t length) { -+ ICMPMsgPtr msg(new ICMPMsg()); -+ if (length < sizeof(struct ip)) { -+ isc_throw(BadValue, -+ "ICMPMsg::unpack - truncated ip header, length: " -+ << length); -+ } -+ -+ // Find the IP header length... -+ struct ip* ip_header = (struct ip*)(wire_data); -+ auto hlen = (ip_header->ip_hl << 2); -+ -+ // Make sure we received enough data. -+ if (length < (hlen + sizeof(struct icmp))) { -+ isc_throw(BadValue, "ICMPMsg::truncated packet? length: " -+ << length << ", hlen: " << hlen); -+ } -+ -+ // Grab the source and destination addresses. -+ msg->setSource(IOAddress(ntohl(ip_header->ip_src.s_addr))); -+ msg->setDestination(IOAddress(ntohl(ip_header->ip_dst.s_addr))); -+ -+ // Get the message type. -+ struct icmp* reply = (struct icmp*)(wire_data + hlen); -+ msg->setType(reply->icmp_type); -+ msg->setCode(reply->icmp_code); -+ -+ msg->setChecksum(ntohs(reply->icmp_cksum)); -+ msg->setId(ntohs(reply->icmp_hun.ih_idseq.icd_id)); -+ msg->setSequence(ntohs(reply->icmp_hun.ih_idseq.icd_seq)); -+ -+ auto payload_len = length - hlen - ICMP_HEADER_SIZE; -+ msg->setPayload((const uint8_t*)(&reply->icmp_dun), payload_len); -+ -+ return (msg); -+} -+ -+ICMPPtr -+ICMPMsg::pack() const { -+ ICMPPtr outbound(new struct icmp()); -+ memset(outbound.get(), 0x00, sizeof(struct icmp)); -+ outbound->icmp_type = msg_type_; -+ outbound->icmp_id = htons(id_); -+ outbound->icmp_seq = htons(sequence_); -+ /// @todo copy in payload - not needed for ECHO REQUEST -+ outbound->icmp_cksum = htons(~calcChecksum((const uint8_t*)(outbound.get()), sizeof(struct icmp))); -+ return (outbound); -+} -+ -+void -+ICMPMsg::setPayload(const uint8_t* data, size_t length) { -+ payload_.insert(payload_.end(), data, data + length); -+} -+ -+uint32_t -+ICMPMsg::calcChecksum(const uint8_t* buf, size_t length) { -+ uint32_t sum = 0; -+ -+ /* Checksum all the pairs of bytes first... */ -+ size_t i; -+ for (i = 0; i < (length & ~1U); i += 2) { -+ sum += static_cast<uint32_t>(readUint16(buf + i, sizeof(uint16_t))); -+ /* Add carry. */ -+ if (sum > 0xFFFF) { -+ sum -= 0xFFFF; -+ } -+ } -+ -+ /* If there's a single byte left over, checksum it, too. Network -+ byte order is big-endian, so the remaining byte is the high byte. */ -+ if (i < length) { -+ sum += buf[i] << 8; -+ /* Add carry. */ -+ if (sum > 0xFFFF) { -+ sum -= 0xFFFF; -+ } -+ } -+ -+ return (sum); -+} -+ -+} // end of namespace ping_check -+} // end of namespace isc -diff --git a/src/hooks/dhcp/ping_check/icmp_msg.h b/src/hooks/dhcp/ping_check/icmp_msg.h -new file mode 100644 -index 0000000000..ace322d1ca ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/icmp_msg.h -@@ -0,0 +1,223 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#ifndef ICMP_MSG_H -+#define ICMP_MSG_H -+ -+#include <asiolink/io_address.h> -+ -+#include <arpa/inet.h> -+#include <netinet/in.h> -+#include <netinet/ip.h> -+#include <unistd.h> -+#include <netinet/ip_icmp.h> -+#include <boost/shared_ptr.hpp> -+ -+namespace isc { -+namespace ping_check { -+ -+// Forward class definition. -+class ICMPMsg; -+ -+/// @brief Shared pointer type for ICMPMsg. -+typedef boost::shared_ptr<ICMPMsg> ICMPMsgPtr; -+ -+/// @brief Shared pointer type for struct icmp. -+typedef boost::shared_ptr<struct icmp> ICMPPtr; -+ -+/// @brief Embodies an ICMP message -+/// -+/// Provides functions for marshalling of ICMP protocol -+/// messages to and from wire form -+class ICMPMsg { -+public: -+ /// @brief ICMP message types. We only define the ones -+ /// we care about. -+ enum ICMPMsgType { -+ ECHO_REPLY = 0, -+ TARGET_UNREACHABLE = 3, -+ ECHO_REQUEST = 8 -+ }; -+ -+ /// @brief Size in octets of ICMP message header. -+ /// 1 (msg type) + 1 (code) + 2 (checksum) + 4 (either unused -+ /// or used differently basing on the ICMP type and code e.g -+ /// Identifier and Sequence Number for Echo or Echo Reply Message) -+ constexpr static size_t ICMP_HEADER_SIZE = 8; -+ -+ /// @brief Constructor. -+ ICMPMsg(); -+ -+ /// @brief Destructor. -+ virtual ~ICMPMsg() = default; -+ -+ /// @brief Unpacks an ICMP message from the given wire_data -+ /// -+ /// The wire data is expected to include the IP header followed -+ /// by an ICMP message. -+ /// -+ /// @param wire_data raw data received from the socket -+ /// @param length number of bytes in the wire_data contents -+ /// -+ /// @return Pointer to the newly constructed message -+ /// @throw BadValue if the wire data is invalid -+ static ICMPMsgPtr unpack(const uint8_t* wire_data, size_t length); -+ -+ /// @brief Packs the message into an ICMP structure. -+ /// -+ /// @return Pointer to the newly constructed ICMP structure. -+ ICMPPtr pack() const; -+ -+ /// @brief Fetches the ICMP message type (e.g. ECHO_REQUEST, ECHO_REPLY) -+ /// -+ /// @return message type as a uint8_t -+ uint8_t getType() const { -+ return (msg_type_); -+ } -+ -+ /// @brief Sets the ICMP message type -+ /// -+ /// @param msg_type new value for the message type -+ void setType(uint8_t msg_type) { -+ msg_type_ = msg_type; -+ } -+ -+ /// @brief Fetches the ICMP message code -+ /// -+ /// @return uint8_t containing the message code -+ uint8_t getCode() const { -+ return (code_); -+ } -+ -+ /// @brief Sets the ICMP code -+ /// -+ /// @param code new value for the message type -+ void setCode(uint8_t code) { -+ code_ = code; -+ } -+ -+ /// @brief Fetches the checksum -+ /// -+ /// @return uint16_t containing the message checksum -+ uint16_t getChecksum() const { -+ return (check_sum_); -+ } -+ -+ /// @brief Sets the check sum -+ /// -+ /// @param check_sum new value for the check sum -+ void setChecksum(uint16_t check_sum) { -+ check_sum_ = check_sum; -+ } -+ -+ /// @brief Fetches the message id -+ /// -+ /// @return uint16_t containing the id -+ uint16_t getId() const { -+ return (id_); -+ } -+ -+ /// @brief Sets the message id -+ /// -+ /// @param id new value for the message id -+ void setId(const uint16_t id) { -+ id_ = id; -+ } -+ -+ /// @brief Fetches the message sequence number -+ /// -+ /// @return uint16_t containing the sequence number -+ uint16_t getSequence() const { -+ return (sequence_); -+ } -+ -+ /// @brief Sets the message sequence number -+ /// -+ /// @param sequence new value for the message sequence number -+ void setSequence(uint16_t sequence) { -+ sequence_ = sequence; -+ } -+ -+ /// @brief Fetches the source IP address -+ /// -+ /// @return IOAddress containing the IP address of the message source -+ const isc::asiolink::IOAddress& getSource() const { -+ return (source_); -+ } -+ -+ /// @brief Sets the source IP address -+ /// -+ /// @param source new value for the source IP address -+ void setSource(const isc::asiolink::IOAddress& source) { -+ source_ = source; -+ } -+ -+ /// @brief Fetches the destination IP address -+ /// -+ /// @return IOAddress containing the IP address of the message destination -+ const isc::asiolink::IOAddress& getDestination() const { -+ return (destination_); -+ } -+ -+ /// @brief Sets the destination IP address -+ /// -+ /// @param destination new value for the destination IP address -+ void setDestination(const isc::asiolink::IOAddress& destination) { -+ destination_ = destination; -+ } -+ -+ /// @brief Fetches the message payload -+ /// -+ /// @return vector containing the message payload -+ const std::vector<uint8_t>& getPayload() const { -+ return (payload_); -+ } -+ -+ /// @brief Sets the message payload to the given data -+ /// -+ /// @param data pointer to data buffer from which to copy -+ /// @param length number of bytes in data buffer -+ void setPayload(const uint8_t* data, size_t length); -+ -+ /// @brief Calculates the checksum of the given data buffer -+ /// -+ /// @param data pointer to data buffer from which to copy -+ /// @param length number of bytes in data buffer -+ /// -+ /// @return uint32_t containing the calculated checksum -+ static uint32_t calcChecksum(const uint8_t* data, size_t length); -+ -+private: -+ /// @brief IP address from which the message origin -+ isc::asiolink::IOAddress source_; -+ -+ /// @brief IP address of the message destination -+ isc::asiolink::IOAddress destination_; -+ -+ /// @brief ICMP message type -+ uint8_t msg_type_; -+ -+ /// @brief ICMP message code -+ uint8_t code_; -+ -+ /// @brief Checksum of the message -+ uint16_t check_sum_; -+ -+ /// @brief Message ID -+ uint16_t id_; -+ -+ /// @brief Message sequence number -+ uint16_t sequence_; -+ -+ // data beyond the ICMP header -+ std::vector<uint8_t> payload_; -+}; -+ -+ -+} // end of namespace ping_check -+} // end of namespace isc -+ -+#endif -diff --git a/src/hooks/dhcp/ping_check/icmp_socket.h b/src/hooks/dhcp/ping_check/icmp_socket.h -new file mode 100644 -index 0000000000..091057d749 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/icmp_socket.h -@@ -0,0 +1,359 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#ifndef ICMP_SOCKET_H -+#define ICMP_SOCKET_H 1 -+ -+#include <netinet/in.h> -+#include <sys/socket.h> -+#include <unistd.h> -+ -+#include <cstddef> -+ -+#include <asiolink/io_asio_socket.h> -+#include <asiolink/io_service.h> -+#include <icmp_endpoint.h> -+ -+#include <exceptions/isc_assert.h> -+ -+namespace isc { -+namespace ping_check { -+ -+/// @brief The @c ICMPSocket class is a concrete derived class of @c IOAsioSocket -+/// that represents a ICMP socket. -+/// -+/// @param C Callback type -+template <typename C> -+class ICMPSocket : public asiolink::IOAsioSocket<C> { -+private: -+ /// @brief Class is non-copyable -+ explicit ICMPSocket(const ICMPSocket&); -+ ICMPSocket& operator=(const ICMPSocket&); -+ -+public: -+ enum { -+ MIN_SIZE = 4096 // Minimum send and receive size -+ }; -+ -+ /// @brief Constructor from an ASIO ICMP socket. -+ /// -+ /// @param socket The ASIO representation of the ICMP socket. It is assumed -+ /// that the caller will open and close the socket, so these -+ /// operations are a no-op for that socket. -+ explicit ICMPSocket(boost::asio::ip::icmp::socket& socket); -+ -+ /// @brief Constructor -+ /// -+ /// Used when the ICMPSocket is being asked to manage its own internal -+ /// socket. In this case, the open() and close() methods are used. -+ /// -+ /// @param service I/O Service object used to manage the socket. -+ explicit ICMPSocket(const asiolink::IOServicePtr& service); -+ -+ /// @brief Destructor -+ virtual ~ICMPSocket(); -+ -+ /// @brief Return file descriptor of underlying socket -+ /// -+ /// @return socket's native file descriptor as an int. -+ virtual int getNative() const { -+#if BOOST_VERSION < 106600 -+ return (socket_.native()); -+#else -+ return (socket_.native_handle()); -+#endif -+ } -+ -+ /// @brief Return protocol of socket -+ /// -+ /// @return Always IPPROTO_ICMP. -+ virtual int getProtocol() const { -+ return (IPPROTO_ICMP); -+ } -+ -+ /// @brief Is "open()" synchronous? -+ /// -+ /// Indicates that the opening of a ICMP socket is synchronous. -+ /// @return Always true. -+ virtual bool isOpenSynchronous() const { -+ return true; -+ } -+ -+ /// @brief Indicates if the socket is currently open. -+ /// -+ /// @return true if socket is open. -+ virtual bool isOpen() const { -+ return isopen_; -+ } -+ -+ /// @brief Open Socket -+ /// -+ /// Opens the ICMP socket. This is a synchronous operation. -+ /// -+ /// @param endpoint Endpoint to which the socket will send data. This is -+ /// used to determine the address family that should be used for the -+ /// underlying socket. -+ /// @param callback Unused as the operation is synchronous. -+ virtual void open(const asiolink::IOEndpoint* endpoint, C& callback); -+ -+ /// @brief Send Asynchronously -+ /// -+ /// Calls the underlying socket's async_send_to() method to send a packet of -+ /// data asynchronously to the remote endpoint. The callback will be called -+ /// on completion. -+ /// -+ /// @param data Data to send -+ /// @param length Length of data to send -+ /// @param endpoint Target of the send -+ /// @param callback Callback object. -+ virtual void asyncSend(const void* data, size_t length, -+ const asiolink::IOEndpoint* endpoint, C& callback); -+ -+ /// @brief Receive Asynchronously -+ /// -+ /// Calls the underlying socket's async_receive_from() method to read a -+ /// packet of data from a remote endpoint. Arrival of the data is signalled -+ /// via a call to the callback function. -+ /// -+ /// @param data Buffer to receive incoming message -+ /// @param length Length of the data buffer -+ /// @param offset Offset into buffer where data is to be put -+ /// @param endpoint Source of the communication -+ /// @param callback Callback object -+ virtual void asyncReceive(void* data, size_t length, size_t offset, -+ asiolink::IOEndpoint* endpoint, C& callback); -+ -+ /// @brief Process received data -+ /// -+ /// See the description of IOAsioSocket::receiveComplete for a complete -+ /// description of this method. -+ /// -+ /// @param staging Pointer to the start of the staging buffer. -+ /// @param length Amount of data in the staging buffer. -+ /// @param cumulative Amount of data received before the staging buffer is -+ /// processed. -+ /// @param offset Unused. -+ /// @param expected unused. -+ /// @param outbuff Output buffer. Data in the staging buffer is be copied -+ /// to this output buffer in the call. -+ /// -+ /// @return Always true -+ virtual bool processReceivedData(const void* staging, size_t length, -+ size_t& cumulative, size_t& offset, -+ size_t& expected, -+ isc::util::OutputBufferPtr& outbuff); -+ -+ /// @brief Cancel I/O On Socket -+ virtual void cancel(); -+ -+ /// @brief Close socket -+ virtual void close(); -+ -+ /// @brief Calculates the checksum for the given buffer of data. -+ /// -+ /// @param buf pointer to the data buffer. -+ /// @param buf_size number of bytes in the data buffer. -+ /// -+ /// @return calculated checksum of the data as a uint16_t. -+ static uint16_t calcChecksum(const uint8_t* buf, const uint32_t buf_size); -+ -+private: -+ /// @brief The IO service used to handle events. -+ isc::asiolink::IOServicePtr io_service_; -+ -+ // Two variables to hold the socket - a socket and a pointer to it. This -+ // handles the case where a socket is passed to the ICMPSocket on -+ // construction, or where it is asked to manage its own socket. -+ -+ /// Pointer to own socket -+ std::unique_ptr<boost::asio::ip::icmp::socket> socket_ptr_; -+ -+ // Socket -+ boost::asio::ip::icmp::socket& socket_; -+ -+ // True when socket is open -+ bool isopen_; -+}; -+ -+// Constructor - caller manages socket -+ -+template <typename C> -+ICMPSocket<C>::ICMPSocket(boost::asio::ip::icmp::socket& socket) : -+ socket_ptr_(), socket_(socket), isopen_(true) { -+} -+ -+// Constructor - create socket on the fly -+ -+template <typename C> -+ICMPSocket<C>::ICMPSocket(const asiolink::IOServicePtr& io_service) : -+ io_service_(io_service), -+ socket_ptr_(new boost::asio::ip::icmp::socket(io_service_->getInternalIOService())), -+ socket_(*socket_ptr_), isopen_(false) { -+} -+ -+// Destructor. -+ -+template <typename C> -+ICMPSocket<C>::~ICMPSocket() { -+} -+ -+// Open the socket. -+ -+template <typename C> void -+ICMPSocket<C>::open(const asiolink::IOEndpoint* endpoint, C&) { -+ -+ // Ignore opens on already-open socket. (Don't throw a failure because -+ // of uncertainties as to what precedes when using asynchronous I/O.) -+ // It also allows us a treat a passed-in socket in exactly the same way as -+ // a self-managed socket (in that we can call the open() and close() methods -+ // of this class). -+ if (!isopen_) { -+ if (endpoint->getFamily() == AF_INET) { -+ socket_.open(boost::asio::ip::icmp::v4()); -+ } else { -+ socket_.open(boost::asio::ip::icmp::v6()); -+ } -+ isopen_ = true; -+ -+ // Ensure it can send and receive at least 4K buffers. -+ boost::asio::ip::icmp::socket::send_buffer_size snd_size; -+ socket_.get_option(snd_size); -+ if (snd_size.value() < MIN_SIZE) { -+ snd_size = MIN_SIZE; -+ socket_.set_option(snd_size); -+ } -+ -+ boost::asio::ip::icmp::socket::receive_buffer_size rcv_size; -+ socket_.get_option(rcv_size); -+ if (rcv_size.value() < MIN_SIZE) { -+ rcv_size = MIN_SIZE; -+ socket_.set_option(rcv_size); -+ } -+ -+ boost::asio::socket_base::do_not_route option(true); -+ socket_.set_option(option); -+ } -+} -+ -+// Send a message. Should never do this if the socket is not open, so throw -+// an exception if this is the case. -+ -+template <typename C> void -+ICMPSocket<C>::asyncSend(const void* data, size_t length, -+ const asiolink::IOEndpoint* endpoint, C& callback) { -+ if (isopen_) { -+ -+ // Upconvert to a ICMPEndpoint. We need to do this because although -+ // IOEndpoint is the base class of ICMPEndpoint and TCPEndpoint, it -+ // does not contain a method for getting at the underlying endpoint -+ // type - that is in the derived class and the two classes differ on -+ // return type. -+ isc_throw_assert(endpoint->getProtocol() == IPPROTO_ICMP); -+ const ICMPEndpoint* udp_endpoint = -+ static_cast<const ICMPEndpoint*>(endpoint); -+ -+ // ... and send the message. -+ socket_.async_send_to(boost::asio::buffer(data, length), -+ udp_endpoint->getASIOEndpoint(), callback); -+ } else { -+ isc_throw(asiolink::SocketNotOpen, -+ "attempt to send on a ICMP socket that is not open"); -+ } -+} -+ -+// Receive a message. Should never do this if the socket is not open, so throw -+// an exception if this is the case. -+ -+template <typename C> void -+ICMPSocket<C>::asyncReceive(void* data, size_t length, size_t offset, -+ asiolink::IOEndpoint* endpoint, C& callback) { -+ if (isopen_) { -+ -+ // Upconvert the endpoint again. -+ isc_throw_assert(endpoint->getProtocol() == IPPROTO_ICMP); -+ ICMPEndpoint* udp_endpoint = static_cast<ICMPEndpoint*>(endpoint); -+ -+ // Ensure we can write into the buffer -+ if (offset >= length) { -+ isc_throw(asiolink::BufferOverflow, "attempt to read into area beyond end of " -+ "ICMP receive buffer"); -+ } -+ void* buffer_start = static_cast<void*>(static_cast<uint8_t*>(data) + offset); -+ -+ // Issue the read -+ socket_.async_receive_from(boost::asio::buffer(buffer_start, length - offset), -+ udp_endpoint->getASIOEndpoint(), callback); -+ } else { -+ isc_throw(asiolink::SocketNotOpen, -+ "attempt to receive from a ICMP socket that is not open"); -+ } -+} -+ -+// Receive complete. Just copy the data across to the output buffer and -+// update arguments as appropriate. -+ -+template <typename C> bool -+ICMPSocket<C>::processReceivedData(const void* staging, size_t length, -+ size_t& cumulative, size_t& offset, -+ size_t& expected, -+ isc::util::OutputBufferPtr& outbuff) { -+ // Set return values to what we should expect. -+ cumulative = length; -+ expected = length; -+ offset = 0; -+ -+ // Copy data across -+ outbuff->writeData(staging, length); -+ -+ // ... and mark that we have everything. -+ return (true); -+} -+ -+// Cancel I/O on the socket. No-op if the socket is not open. -+ -+template <typename C> void -+ICMPSocket<C>::cancel() { -+ if (isopen_) { -+ socket_.cancel(); -+ } -+} -+ -+// Close the socket down. Can only do this if the socket is open and we are -+// managing it ourself. -+ -+template <typename C> void -+ICMPSocket<C>::close() { -+ if (isopen_ && socket_ptr_) { -+ socket_.close(); -+ isopen_ = false; -+ } -+} -+ -+template <typename C> uint16_t -+ICMPSocket<C>::calcChecksum(const uint8_t* buf, const uint32_t buf_size) { -+ uint32_t sum = 0; -+ uint32_t i; -+ for (i = 0; i < (buf_size & ~1U); i += 2) { -+ uint16_t chunk = buf[i] << 8 | buf[i + 1]; -+ sum += chunk; -+ if (sum > 0xFFFF) { -+ sum -= 0xFFFF; -+ } -+ } -+ // If one byte has left, we also need to add it to the checksum. -+ if (i < buf_size) { -+ sum += buf[i] << 8; -+ if (sum > 0xFFFF) { -+ sum -= 0xFFFF; -+ } -+ } -+ -+ return (sum); -+} -+ -+} // namespace ping_check -+} // namespace isc -+#endif // ICMP_SOCKET_H -diff --git a/src/hooks/dhcp/ping_check/libloadtests/.gitignore b/src/hooks/dhcp/ping_check/libloadtests/.gitignore -new file mode 100644 -index 0000000000..ada6ed5036 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/libloadtests/.gitignore -@@ -0,0 +1 @@ -+hook_load_unittests -diff --git a/src/hooks/dhcp/ping_check/libloadtests/Makefile.am b/src/hooks/dhcp/ping_check/libloadtests/Makefile.am -new file mode 100644 -index 0000000000..139a068b3c ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/libloadtests/Makefile.am -@@ -0,0 +1,60 @@ -+SUBDIRS = . -+ -+AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib -+AM_CPPFLAGS += -I$(top_builddir)/src/hooks/dhcp/ping_check -I$(top_srcdir)/src/hooks/dhcp/ping_check -+AM_CPPFLAGS += $(BOOST_INCLUDES) $(CRYPTO_CFLAGS) $(CRYPTO_INCLUDES) -+AM_CPPFLAGS += -DPING_CHECK_LIB_SO=\"$(abs_top_builddir)/src/hooks/dhcp/ping_check/.libs/libdhcp_ping_check.so\" -+AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\" -+ -+AM_CXXFLAGS = $(KEA_CXXFLAGS) -+ -+if USE_STATIC_LINK -+AM_LDFLAGS = -static -+endif -+ -+# Unit test data files need to get installed. -+EXTRA_DIST = -+ -+CLEANFILES = *.gcno *.gcda -+ -+TESTS_ENVIRONMENT = $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND) -+ -+LOG_COMPILER = $(LIBTOOL) -+AM_LOG_FLAGS = --mode=execute -+ -+TESTS = -+if HAVE_GTEST -+TESTS += hook_load_unittests -+ -+hook_load_unittests_SOURCES = run_unittests.cc -+hook_load_unittests_SOURCES += load_unload_unittests.cc -+ -+hook_load_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) $(LOG4CPLUS_INCLUDES) -+ -+hook_load_unittests_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) $(GTEST_LDFLAGS) -+ -+hook_load_unittests_CXXFLAGS = $(AM_CXXFLAGS) -+ -+hook_load_unittests_LDADD = $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la -+hook_load_unittests_LDADD += $(top_builddir)/src/lib/process/libkea-process.la -+hook_load_unittests_LDADD += $(top_builddir)/src/lib/eval/libkea-eval.la -+hook_load_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la -+hook_load_unittests_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la -+hook_load_unittests_LDADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la -+hook_load_unittests_LDADD += $(top_builddir)/src/lib/http/libkea-http.la -+hook_load_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la -+hook_load_unittests_LDADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la -+hook_load_unittests_LDADD += $(top_builddir)/src/lib/database/libkea-database.la -+hook_load_unittests_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la -+hook_load_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la -+hook_load_unittests_LDADD += $(top_builddir)/src/lib/dns/libkea-dns++.la -+hook_load_unittests_LDADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la -+hook_load_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la -+hook_load_unittests_LDADD += $(top_builddir)/src/lib/util/libkea-util.la -+hook_load_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la -+hook_load_unittests_LDADD += $(LOG4CPLUS_LIBS) -+hook_load_unittests_LDADD += $(CRYPTO_LIBS) -+hook_load_unittests_LDADD += $(BOOST_LIBS) -+hook_load_unittests_LDADD += $(GTEST_LDADD) -+endif -+noinst_PROGRAMS = $(TESTS) -diff --git a/src/hooks/dhcp/ping_check/libloadtests/load_unload_unittests.cc b/src/hooks/dhcp/ping_check/libloadtests/load_unload_unittests.cc -new file mode 100644 -index 0000000000..67275db617 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/libloadtests/load_unload_unittests.cc -@@ -0,0 +1,107 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+/// @file This file contains tests which exercise the load and unload -+/// functions in the ddns tuning hook library. In order to test the load -+/// function, one must be able to pass it hook library parameters. The -+/// the only way to populate these parameters is by actually loading the -+/// library via HooksManager::loadLibraries(). -+ -+#include <config.h> -+ -+#include <dhcpsrv/testutils/lib_load_test_fixture.h> -+#include <testutils/gtest_utils.h> -+ -+#include <gtest/gtest.h> -+#include <errno.h> -+ -+using namespace std; -+using namespace isc; -+using namespace isc::hooks; -+using namespace isc::data; -+using namespace isc::dhcp; -+using namespace isc::process; -+ -+namespace { -+ -+/// @brief Test fixture for testing loading and unloading the ddns tuning library -+class PingCheckLibLoadTest : public isc::test::LibLoadTest { -+public: -+ /// @brief Constructor -+ PingCheckLibLoadTest() : LibLoadTest(PING_CHECK_LIB_SO) { -+ } -+ -+ /// @brief Destructor -+ virtual ~PingCheckLibLoadTest() { -+ } -+ -+ /// @brief Registers hooks in the hook manager. -+ /// Normally this is done by the server core code (@c Dhcpv4Srv). -+ void registerHooks() { -+ hook_index_dhcp4_srv_configured_ = HooksManager::registerHook("dhcp4_srv_configured"); -+ hook_index_lease4_offer_ = HooksManager::registerHook("lease4_offer"); -+ } -+ -+ /// @brief Checks that expected callouts are present. -+ void calloutsPresent() { -+ bool result; -+ ASSERT_NO_THROW_LOG(result = HooksManager::calloutsPresent(hook_index_dhcp4_srv_configured_)); -+ EXPECT_TRUE(result); -+ ASSERT_NO_THROW_LOG(result = HooksManager::calloutsPresent(hook_index_lease4_offer_)); -+ EXPECT_TRUE(result); -+ } -+ -+ /// @brief Creates a valid set of ping-check hook parameters. -+ virtual ElementPtr validConfigParams() { -+ ElementPtr params = Element::createMap(); -+ params->set("min-ping-requests", Element::create(3)); -+ params->set("reply-timeout", Element::create(100)); -+ params->set("enable-ping-check", Element::create(true)); -+ params->set("ping-cltt-secs", Element::create(60)); -+ params->set("ping-channel-threads", Element::create(1)); -+ return (params); -+ } -+ -+ /// @brief Hook index values. -+ int hook_index_dhcp4_srv_configured_; -+ int hook_index_lease4_offer_; -+}; -+ -+// Simple V4 test that checks the library can be loaded and unloaded several times. -+TEST_F(PingCheckLibLoadTest, validLoad4) { -+ validDaemonTest("kea-dhcp4", AF_INET, valid_params_); -+} -+ -+// Simple test that checks the library cannot be loaded by invalid daemons. -+TEST_F(PingCheckLibLoadTest, invalidDaemonLoad) { -+ // V6 is invalid regardless of family. -+ invalidDaemonTest("kea-dhcp6", AF_INET, valid_params_); -+ invalidDaemonTest("kea-dhcp6", AF_INET6, valid_params_); -+ -+ invalidDaemonTest("kea-ctrl-agent", AF_INET, valid_params_); -+ invalidDaemonTest("kea-dhcp-ddns", AF_INET, valid_params_); -+ invalidDaemonTest("bogus", AF_INET, valid_params_); -+} -+ -+// Verifies that callout functions exist after loading the library. -+TEST_F(PingCheckLibLoadTest, verifyCallouts) { -+ // Set family and daemon's proc name and register hook points. -+ isc::dhcp::CfgMgr::instance().setFamily(AF_INET); -+ isc::process::Daemon::setProcName("kea-dhcp4"); -+ registerHooks(); -+ -+ // Add library to config and load it. -+ ASSERT_NO_THROW_LOG(addLibrary(lib_so_name_, valid_params_)); -+ ASSERT_NO_THROW_LOG(loadLibraries()); -+ -+ // Verify that expected callouts are present. -+ calloutsPresent(); -+ -+ // Unload the library. -+ ASSERT_NO_THROW_LOG(unloadLibraries()); -+} -+ -+} // end of anonymous namespace -diff --git a/src/hooks/dhcp/ping_check/libloadtests/meson.build b/src/hooks/dhcp/ping_check/libloadtests/meson.build -new file mode 100644 -index 0000000000..da8bf439c0 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/libloadtests/meson.build -@@ -0,0 +1,21 @@ -+if not TESTS_OPT.enabled() -+ subdir_done() -+endif -+ -+dhcp_ping_check_libloadtests = executable( -+ 'dhcp-ping-check-libload-tests', -+ 'load_unload_unittests.cc', -+ 'run_unittests.cc', -+ cpp_args: [ -+ f'-DPING_CHECK_LIB_SO="@TOP_BUILD_DIR@/src/hooks/dhcp/ping_check/libdhcp_ping_check.so"', -+ ], -+ dependencies: [GTEST_DEP, CRYPTO_DEP], -+ include_directories: [include_directories('.')] + INCLUDES, -+ link_with: LIBS_BUILT_SO_FAR, -+) -+test( -+ 'dhcp-ping-check-libloadtests', -+ dhcp_ping_check_libloadtests, -+ depends: [dhcp_ping_check_lib], -+ protocol: 'gtest', -+) -diff --git a/src/hooks/dhcp/ping_check/libloadtests/run_unittests.cc b/src/hooks/dhcp/ping_check/libloadtests/run_unittests.cc -new file mode 100644 -index 0000000000..d249e2362e ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/libloadtests/run_unittests.cc -@@ -0,0 +1,19 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#include <config.h> -+ -+#include <log/logger_support.h> -+#include <gtest/gtest.h> -+ -+int -+main(int argc, char* argv[]) { -+ ::testing::InitGoogleTest(&argc, argv); -+ isc::log::initLogger(); -+ int result = RUN_ALL_TESTS(); -+ -+ return (result); -+} -diff --git a/src/hooks/dhcp/ping_check/meson.build b/src/hooks/dhcp/ping_check/meson.build -new file mode 100644 -index 0000000000..d3a1e70b49 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/meson.build -@@ -0,0 +1,41 @@ -+dhcp_ping_check_lib = shared_library( -+ 'dhcp_ping_check', -+ 'config_cache.cc', -+ 'icmp_msg.cc', -+ 'ping_channel.cc', -+ 'ping_check_callouts.cc', -+ 'ping_check_config.cc', -+ 'ping_check_log.cc', -+ 'ping_check_messages.cc', -+ 'ping_check_mgr.cc', -+ 'ping_context.cc', -+ 'ping_context_store.cc', -+ 'version.cc', -+ dependencies: [CRYPTO_DEP], -+ include_directories: [include_directories('.')] + INCLUDES, -+ install: true, -+ install_dir: HOOKS_PATH, -+ install_rpath: INSTALL_RPATH, -+ build_rpath: BUILD_RPATH, -+ link_with: LIBS_BUILT_SO_FAR, -+ name_suffix: 'so', -+) -+dhcp_ping_check_archive = static_library( -+ 'dhcp_ping_check', -+ objects: dhcp_ping_check_lib.extract_all_objects(recursive: false), -+) -+subdir('libloadtests') -+subdir('tests') -+ -+if KEA_MSG_COMPILER.found() -+ target_gen_messages = run_target( -+ 'src-hooks-dhcp-ping_check-ping_check_messages', -+ command: [ -+ CD_AND_RUN, -+ TOP_SOURCE_DIR, -+ KEA_MSG_COMPILER, -+ 'src/hooks/dhcp/ping_check/ping_check_messages.mes', -+ ], -+ ) -+ TARGETS_GEN_MESSAGES += [target_gen_messages] -+endif -diff --git a/src/hooks/dhcp/ping_check/ping_channel.cc b/src/hooks/dhcp/ping_check/ping_channel.cc -new file mode 100644 -index 0000000000..6a6a88c038 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/ping_channel.cc -@@ -0,0 +1,466 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#include <config.h> -+#include <ping_channel.h> -+#include <ping_check_log.h> -+#include <dhcp/iface_mgr.h> -+#include <exceptions/exceptions.h> -+#include <util/multi_threading_mgr.h> -+#include <iostream> -+ -+using namespace isc; -+using namespace isc::asiolink; -+using namespace isc::dhcp; -+using namespace isc::util; -+ -+namespace ph = std::placeholders; -+ -+namespace isc { -+namespace ping_check { -+ -+uint32_t -+PingChannel::nextEchoInstanceNum() { -+ static uint32_t echo_instance_num = 0x00010000; -+ if (echo_instance_num == UINT32_MAX) { -+ echo_instance_num = 0x00010001; -+ } else { -+ ++echo_instance_num; -+ } -+ -+ return (echo_instance_num); -+} -+ -+PingChannel::PingChannel(IOServicePtr& io_service, -+ NextToSendCallback next_to_send_cb, -+ EchoSentCallback echo_sent_cb, -+ ReplyReceivedCallback reply_received_cb, -+ ShutdownCallback shutdown_cb) -+ : io_service_(io_service), -+ next_to_send_cb_(next_to_send_cb), -+ echo_sent_cb_(echo_sent_cb), -+ reply_received_cb_(reply_received_cb), -+ shutdown_cb_(shutdown_cb), -+ socket_(0), input_buf_(256), -+ reading_(false), sending_(false), stopping_(false), mutex_(new std::mutex), -+ single_threaded_(!MultiThreadingMgr::instance().getMode()), -+ watch_socket_(0), registered_write_fd_(-1), registered_read_fd_(-1) { -+ if (!io_service_) { -+ isc_throw(BadValue, -+ "PingChannel ctor - io_service cannot be empty"); -+ } -+} -+ -+PingChannel::~PingChannel() { -+ close(); -+} -+ -+void -+PingChannel::open() { -+ try { -+ MultiThreadingLock lock(*mutex_); -+ if (socket_ && socket_->isOpen()) { -+ return; -+ } -+ -+ // For open(), the endpoint is only used to determine protocol, -+ // the address is irrelevant. -+ ICMPEndpoint ping_to_endpoint(IOAddress::IPV4_ZERO_ADDRESS()); -+ SocketCallback socket_cb( -+ [](boost::system::error_code ec, size_t /*length */) { -+ isc_throw(Unexpected, "ICMPSocket open is synchronous, should not invoke cb: " -+ << ec.message()); -+ } -+ ); -+ -+ socket_.reset(new PingSocket(io_service_)); -+ socket_->open(&ping_to_endpoint, socket_cb); -+ reading_ = false; -+ sending_ = false; -+ stopping_ = false; -+ -+ if (single_threaded_) { -+ // Open new watch socket. -+ watch_socket_.reset(new util::WatchSocket()); -+ -+ // Register the WatchSocket with IfaceMgr to signal data ready to write. -+ registered_write_fd_ = watch_socket_->getSelectFd(); -+ IfaceMgr::instance().addExternalSocket(registered_write_fd_, IfaceMgr::SocketCallback()); -+ -+ // Register ICMPSocket with IfaceMgr to signal data ready to read. -+ registered_read_fd_ = socket_->getNative(); -+ IfaceMgr::instance().addExternalSocket(registered_read_fd_, IfaceMgr::SocketCallback()); -+ } -+ -+ } catch (const std::exception& ex) { -+ isc_throw(Unexpected, "PingChannel::open failed:" << ex.what()); -+ } -+ -+ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC, PING_CHECK_CHANNEL_SOCKET_OPENED); -+} -+ -+bool -+PingChannel::isOpen() const { -+ MultiThreadingLock lock(*mutex_); -+ return (socket_ && socket_->isOpen()); -+} -+ -+void -+PingChannel::close() { -+ try { -+ MultiThreadingLock lock(*mutex_); -+ -+ if (single_threaded_) { -+ // Unregister from IfaceMgr. -+ if (registered_write_fd_ != -1) { -+ IfaceMgr::instance().deleteExternalSocket(registered_write_fd_); -+ registered_write_fd_ = -1; -+ } -+ -+ if (registered_read_fd_ != -1) { -+ IfaceMgr::instance().deleteExternalSocket(registered_read_fd_); -+ registered_read_fd_ = -1; -+ } -+ -+ // Close watch socket. -+ if (watch_socket_) { -+ std::string error_string; -+ watch_socket_->closeSocket(error_string); -+ if (!error_string.empty()) { -+ LOG_ERROR(ping_check_logger, PING_CHECK_CHANNEL_WATCH_SOCKET_CLOSE_ERROR) -+ .arg(error_string); -+ } -+ -+ watch_socket_.reset(); -+ } -+ } -+ -+ if (!socket_ || !socket_->isOpen()) { -+ return; -+ } -+ -+ socket_->close(); -+ } catch (const std::exception& ex) { -+ // On close error, log but do not throw. -+ LOG_ERROR(ping_check_logger, PING_CHECK_CHANNEL_SOCKET_CLOSE_ERROR) -+ .arg(ex.what()); -+ } -+ -+ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC, PING_CHECK_CHANNEL_SOCKET_CLOSED); -+} -+ -+void -+PingChannel::stopChannel() { -+ { -+ MultiThreadingLock lock(*mutex_); -+ if (stopping_) { -+ return; -+ } -+ -+ stopping_ = true; -+ } -+ -+ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC, PING_CHECK_CHANNEL_STOP); -+ close(); -+ -+ if (shutdown_cb_) { -+ (shutdown_cb_)(); -+ } -+} -+ -+void -+PingChannel::asyncReceive(void* data, size_t length, size_t offset, -+ asiolink::IOEndpoint* endpoint, SocketCallback& callback) { -+ socket_->asyncReceive(data, length, offset, endpoint, callback); -+} -+ -+void -+PingChannel::asyncSend(void* data, size_t length, asiolink::IOEndpoint* endpoint, -+ SocketCallback& callback) { -+ socket_->asyncSend(data, length, endpoint, callback); -+ -+ if (single_threaded_) { -+ // Set IO ready marker so sender activity is visible to select() or poll(). -+ watch_socket_->markReady(); -+ } -+} -+ -+void -+PingChannel::doRead() { -+ try { -+ MultiThreadingLock lock(*mutex_); -+ if (!canRead()) { -+ return; -+ } -+ -+ reading_ = true; -+ -+ // Create instance of the callback. It is safe to pass the -+ // local instance of the callback, because the underlying -+ // std functions make copies as needed. -+ SocketCallback cb(std::bind(&PingChannel::socketReadCallback, -+ shared_from_this(), -+ ph::_1, // error -+ ph::_2)); // bytes_transferred -+ asyncReceive(static_cast<void*>(getInputBufData()), getInputBufSize(), -+ 0, &reply_endpoint_, cb); -+ } catch (const std::exception& ex) { -+ // Normal IO failures should be passed to the callback. A failure here -+ // indicates the call to asyncReceive() itself failed. -+ LOG_ERROR(ping_check_logger, PING_CHECK_UNEXPECTED_READ_ERROR) -+ .arg(ex.what()); -+ stopChannel(); -+ } -+} -+ -+void -+PingChannel::socketReadCallback(boost::system::error_code ec, size_t length) { -+ { -+ MultiThreadingLock lock(*mutex_); -+ if (stopping_) { -+ return; -+ } -+ } -+ -+ if (ec) { -+ if (ec.value() == boost::asio::error::operation_aborted) { -+ // IO service has been stopped and the connection is probably -+ // going to be shutting down. -+ return; -+ } else if ((ec.value() == boost::asio::error::try_again) || -+ (ec.value() == boost::asio::error::would_block)) { -+ // We got EWOULDBLOCK or EAGAIN which indicates that we may be able to -+ // read something from the socket on the next attempt. Just make sure -+ // we don't try to read anything now in case there is any garbage -+ // passed in length. -+ length = 0; -+ } else { -+ // Anything else is fatal for the socket. -+ LOG_ERROR(ping_check_logger, PING_CHECK_CHANNEL_SOCKET_READ_FAILED) -+ .arg(ec.message()); -+ stopChannel(); -+ return; -+ } -+ } -+ -+ // Unpack the reply and pass it to the reply callback. -+ ICMPMsgPtr reply; -+ if (length > 0) { -+ { -+ try { -+ MultiThreadingLock lock(*mutex_); -+ reply = ICMPMsg::unpack(getInputBufData(), getInputBufSize()); -+ if (reply->getType() == ICMPMsg::ECHO_REPLY) { -+ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_DETAIL, -+ PING_CHECK_CHANNEL_ECHO_REPLY_RECEIVED) -+ .arg(reply->getSource()) -+ .arg(reply->getId()) -+ .arg(reply->getSequence()); -+ } -+ } catch (const std::exception& ex) { -+ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC, -+ PING_CHECK_CHANNEL_MALFORMED_PACKET_RECEIVED) -+ .arg(ex.what()); -+ } -+ } -+ } -+ -+ { -+ MultiThreadingLock lock(*mutex_); -+ reading_ = false; -+ } -+ -+ if (reply) { -+ (reply_received_cb_)(reply); -+ } -+ -+ // Start the next read. -+ doRead(); -+} -+ -+void -+PingChannel::startSend() { -+ MultiThreadingLock lock(*mutex_); -+ if (canSend()) { -+ // Post the call to sendNext to the IOService. -+ // This ensures its carried out on a thread -+ // associated with the channel's IOService -+ // not the thread invoking this function. -+ auto f = [](PingChannelPtr ptr) { ptr->sendNext(); }; -+ io_service_->post(std::bind(f, shared_from_this())); -+ } -+} -+ -+void -+PingChannel::startRead() { -+ MultiThreadingLock lock(*mutex_); -+ if (canRead()) { -+ // Post the call to doRead to the IOService. -+ // This ensures its carried out on a thread -+ // associated with the channel's IOService -+ // not the thread invoking this function. -+ auto f = [](PingChannelPtr ptr) { ptr->doRead(); }; -+ io_service_->post(std::bind(f, shared_from_this())); -+ } -+} -+ -+void -+PingChannel::sendNext() { -+ try { -+ MultiThreadingLock lock(*mutex_); -+ if (!canSend()) { -+ // Can't send right now, get out. -+ return; -+ } -+ -+ // Fetch the next one to send. -+ IOAddress target("0.0.0.0"); -+ if (!((next_to_send_cb_)(target))) { -+ // Nothing to send. -+ return; -+ } -+ -+ // Have an target IP, build an ECHO REQUEST for it. -+ sending_ = true; -+ ICMPMsgPtr next_echo(new ICMPMsg()); -+ next_echo->setType(ICMPMsg::ECHO_REQUEST); -+ next_echo->setDestination(target); -+ -+ uint32_t instance_num = nextEchoInstanceNum(); -+ next_echo->setId(static_cast<uint16_t>(instance_num >> 16)); -+ next_echo->setSequence(static_cast<uint16_t>(instance_num & 0x0000FFFF)); -+ -+ // Get packed wire-form. -+ ICMPPtr echo_icmp = next_echo->pack(); -+ -+ // Create instance of the callback. It is safe to pass the -+ // local instance of the callback, because the underlying -+ // std functions make copies as needed. -+ SocketCallback cb(std::bind(&PingChannel::socketWriteCallback, -+ shared_from_this(), -+ next_echo, -+ ph::_1, // error -+ ph::_2)); // bytes_transferred -+ -+ ICMPEndpoint target_endpoint(target); -+ asyncSend(echo_icmp.get(), sizeof(struct icmp), &target_endpoint, cb); -+ } catch (const std::exception& ex) { -+ // Normal IO failures should be passed to the callback. A failure here -+ // indicates the call to asyncSend() itself failed. -+ LOG_ERROR(ping_check_logger, PING_CHECK_UNEXPECTED_WRITE_ERROR) -+ .arg(ex.what()); -+ stopChannel(); -+ return; -+ } -+} -+ -+void -+PingChannel::socketWriteCallback(ICMPMsgPtr echo, boost::system::error_code ec, -+ size_t length) { -+ { -+ MultiThreadingLock lock(*mutex_); -+ if (stopping_) { -+ return; -+ } -+ } -+ -+ if (single_threaded_) { -+ try { -+ // Clear the IO ready marker. -+ watch_socket_->clearReady(); -+ } catch (const std::exception& ex) { -+ // This can only happen if the WatchSocket's select_fd has been -+ // compromised which is a programmatic error. We'll log the error -+ // here, then continue on and process the IO result we were given. -+ // WatchSocket issue will resurface on the next send as a closed -+ // fd in markReady() rather than fail out of this callback. -+ LOG_ERROR(ping_check_logger, PING_CHECK_CHANNEL_WATCH_SOCKET_CLEAR_ERROR) -+ .arg(ex.what()); -+ } -+ } -+ -+ // Handle an error. Note we can't use a case statement as some values -+ // on some OSes are the same (e.g. try_again and would_block) which causes -+ // duplicate case compilation errors. -+ bool send_failed = false; -+ if (ec) { -+ auto error_value = ec.value(); -+ if (error_value == boost::asio::error::operation_aborted) { -+ // IO service has been stopped and the connection is probably -+ // going to be shutting down. -+ return; -+ } else if ((error_value == boost::asio::error::try_again) || -+ (error_value == boost::asio::error::would_block)) { -+ // We got EWOULDBLOCK or EAGAIN which indicates that we may be able to -+ // write something from the socket on the next attempt. Set the length -+ // to zero so we skip the completion callback. -+ length = 0; -+ } else if ((error_value == boost::asio::error::network_unreachable) || -+ (error_value == boost::asio::error::host_unreachable) || -+ (error_value == boost::asio::error::network_down)) { -+ // One of these implies an interface might be down, or there's no -+ // way to ping this network. Other networks might be working OK. -+ send_failed = true; -+ } else if (error_value == boost::asio::error::no_buffer_space) { -+ // Writing faster than the kernel will write them out. -+ send_failed = true; -+ } else if (error_value == boost::asio::error::access_denied) { -+ // Means the address we tried to ping is not allowed. Most likey a broadcast -+ // address. -+ send_failed = true; -+ } else { -+ // Anything else is fatal for the socket. -+ LOG_ERROR(ping_check_logger, PING_CHECK_CHANNEL_SOCKET_WRITE_FAILED) -+ .arg(ec.message()); -+ stopChannel(); -+ return; -+ } -+ } -+ -+ { -+ MultiThreadingLock lock(*mutex_); -+ sending_ = false; -+ } -+ -+ if (send_failed) { -+ // Invoke the callback with send failed. This instructs the manager -+ // to treat the address as free to use. -+ LOG_ERROR(ping_check_logger, PING_CHECK_CHANNEL_NETWORK_WRITE_ERROR) -+ .arg(echo->getDestination()) -+ .arg(ec.message()); -+ // Invoke the send completed callback. -+ (echo_sent_cb_)(echo, true); -+ } else if (length > 0) { -+ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_DETAIL, -+ PING_CHECK_CHANNEL_ECHO_REQUEST_SENT) -+ .arg(echo->getDestination()) -+ .arg(echo->getId()) -+ .arg(echo->getSequence()); -+ // Invoke the send completed callback. -+ (echo_sent_cb_)(echo, false); -+ } -+ -+ // Schedule the next send. -+ sendNext(); -+} -+ -+size_t -+PingChannel::getInputBufSize() const { -+ return (input_buf_.size()); -+} -+ -+unsigned char* -+PingChannel::getInputBufData() { -+ if (input_buf_.empty()) { -+ isc_throw(InvalidOperation, -+ "PingChannel::getInputBufData() - cannot access empty buffer"); -+ } -+ -+ return (input_buf_.data()); -+} -+ -+} // end of namespace ping_check -+} // end of namespace isc -diff --git a/src/hooks/dhcp/ping_check/ping_channel.h b/src/hooks/dhcp/ping_check/ping_channel.h -new file mode 100644 -index 0000000000..ad798188e3 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/ping_channel.h -@@ -0,0 +1,371 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#ifndef PING_CHANNEL_H -+#define PING_CHANNEL_H -+ -+#include <asiolink/asio_wrapper.h> -+#include <asiolink/io_address.h> -+#include <asiolink/io_service.h> -+#include <util/watch_socket.h> -+#include <icmp_msg.h> -+#include <icmp_socket.h> -+ -+#include <boost/scoped_ptr.hpp> -+#include <boost/enable_shared_from_this.hpp> -+ -+#include <iostream> -+#include <mutex> -+ -+namespace isc { -+namespace ping_check { -+ -+/// @brief Type of the function implementing a callback invoked by the -+/// @c SocketCallback functor. -+typedef std::function<void(boost::system::error_code ec, size_t length)> SocketCallbackFunction; -+ -+/// @brief Functor associated with the socket object. -+/// -+/// This functor calls a callback function specified in the constructor. -+class SocketCallback { -+public: -+ /// @brief Constructor. -+ /// -+ /// @param socket_callback Callback to be invoked by the functor upon -+ /// an event associated with the socket. -+ explicit inline SocketCallback(SocketCallbackFunction socket_callback) -+ : callback_(socket_callback) { -+ }; -+ -+ /// @brief Operator called when event associated with a socket occurs. -+ /// -+ /// This operator returns immediately when received @c boost::system::error_code -+ /// is equal to @c boost::asio::error::operation_aborted. -+ /// -+ /// @param ec Error code. -+ /// @param length Data length. -+ inline void operator()(boost::system::error_code ec, size_t length = 0) { -+ if (ec.value() == boost::asio::error::operation_aborted) { -+ return; -+ } -+ -+ callback_(ec, length); -+ }; -+ -+private: -+ /// @brief Supplied callback. -+ SocketCallbackFunction callback_; -+}; -+ -+/// @brief Socket type for performing ICMP socket IO. -+typedef ICMPSocket<SocketCallback> PingSocket; -+ -+/// @brief Defines a pointer to PingSocket. -+typedef boost::shared_ptr<PingSocket> PingSocketPtr; -+ -+/// @brief Function type for callback that fetches next IOAddress to ping. -+typedef std::function<bool(asiolink::IOAddress& target)> NextToSendCallback; -+ -+/// @brief Function type for callback to invoke upon ECHO send completion. -+typedef std::function<void(ICMPMsgPtr& echo, bool send_failed)> EchoSentCallback; -+ -+/// @brief Function type for callback to invoke when an ICMP reply has been -+/// received. -+typedef std::function<void(ICMPMsgPtr& reply)> ReplyReceivedCallback; -+ -+/// @brief Function type for callback to invoke when the channel has shutdown. -+typedef std::function<void()> ShutdownCallback; -+ -+/// @brief Provides thread-safe ICMP ECHO REQUEST/ECHO REPLY service -+/// -+/// PingChannel uses a @ref PingSocket to send out ECHO REQUESTs and -+/// receive ICMP replies. It is thread-safe and can be driven either -+/// with a single-threaded IOService or a multi-threaded -+/// IOServiceThreadPool. It uses series of callbacks to perpetually -+/// send requests to target addresses and feed back replies received: -+/// -+/// -# next_to_send_cb_ - callback to invoke to fetch the next address to ping -+/// -# echo_sent_cb_ - callback to invoke when an ECHO REQUEST has been sent out -+/// -# reply_received_cb_ - callback to invoke when an ICMP reply has been received. -+/// -# channel_shutdown_cb_ - callback to invoke when the channel has shutdown -+/// -+/// Callback handlers are supplied via the PingChannel constructor. Higher order -+/// functions are provided, that once instantiated, can be used by calling layers -+/// to control the channel (e.g. open the channel, initiate reading, initiate -+/// writing, and close the channel). -+/// -+/// @note Callbacks handlers must be thread-safe if the channel is -+/// driven by an IOServiceThreadPool. -+/// -+class PingChannel : public boost::enable_shared_from_this<PingChannel> { -+public: -+ /// @brief Constructor -+ /// -+ /// Instantiates the channel with its socket closed. -+ /// -+ /// @param io_service pointer to the IOService instance that will manage -+ /// the channel's IO. Must not be empty -+ /// @param next_to_send_cb callback to invoke to fetch the next IOAddress -+ /// to ping -+ /// @param echo_sent_cb callback to invoke when an ECHO send has completed -+ /// @param reply_received_cb callback to invoke when an ICMP reply has been -+ /// received. This callback is passed all inbound ICMP messages (e.g. ECHO -+ /// REPLY, UNREACHABLE, etc...) -+ /// @param shutdown_cb callback to invoke when the channel has shutdown due -+ /// to an error -+ /// -+ /// @throw BadValue if io_service is empty. -+ PingChannel(asiolink::IOServicePtr& io_service, -+ NextToSendCallback next_to_send_cb, -+ EchoSentCallback echo_sent_cb, -+ ReplyReceivedCallback reply_received_cb, -+ ShutdownCallback shutdown_cb = ShutdownCallback()); -+ -+ /// @brief Destructor -+ /// -+ /// Closes the socket if its open. -+ virtual ~PingChannel(); -+ -+ /// @brief Opens the socket for communications -+ /// -+ /// (Re)Creates the @ref PingSocket instance and opens it. -+ /// -+ /// @throw Unexpected if the open fails. -+ void open(); -+ -+ /// @brief Indicates whether or not the channel socket is open. -+ /// -+ /// @return true if the socket is open. -+ bool isOpen() const; -+ -+ // @brief Schedules the next send. -+ // -+ // If the socket is not currently sending it posts a call to @c sendNext() -+ // to the channel's IOService. -+ virtual void startSend(); -+ -+ // @brief Schedules the next read. -+ // -+ // If the socket is not currently reading it posts a call to @c doRead() -+ // to the channel's IOService. -+ void startRead(); -+ -+ /// @brief Closes the channel's socket. -+ void close(); -+ -+ /// @brief Fetches the channel's IOService -+ /// -+ /// @return pointer to the IOService. -+ asiolink::IOServicePtr getIOService() { -+ return (io_service_); -+ } -+ -+protected: -+ /// @brief Receive data on the socket asynchronously -+ /// -+ /// Calls the underlying socket's asyncReceive() method to read a -+ /// packet of data from a remote endpoint. Arrival of the data is signalled -+ /// via a call to the callback function. -+ /// -+ /// This virtual function is provided as means to inject errors during -+ /// read operations to facilitate testing. -+ /// -+ /// @param data buffer to receive incoming message -+ /// @param length length of the data buffer -+ /// @param offset offset into buffer where data is to be put -+ /// @param endpoint source of the communication -+ /// @param callback callback object -+ virtual void asyncReceive(void* data, size_t length, size_t offset, -+ asiolink::IOEndpoint* endpoint, SocketCallback& callback); -+ -+ /// @brief Send data on the socket asynchronously -+ /// -+ /// Calls the underlying socket's asyncSend() method to send a -+ /// packet of data from a remote endpoint. Arrival of the data is signalled -+ /// via a call to the callback function. -+ /// -+ /// This virtual function is provided as means to inject errors during -+ /// write operations to facilitate testing. -+ /// -+ /// @param data buffer containing the data to send -+ /// @param length length of the data buffer -+ /// @param endpoint destination of the communication -+ /// @param callback callback object -+ virtual void asyncSend(void* data, size_t length, asiolink::IOEndpoint* endpoint, -+ SocketCallback& callback); -+ -+protected: -+ /// @brief Initiates an asynchronous socket read. -+ /// -+ /// If the channel is able to read (is open, not stopping and not -+ /// currently reading) it invokes @ref PingSocket::asyncReceive() -+ /// otherwise it simply returns. If the call to asyncReceive() fails -+ /// it calls @c stopChannel() otherwise, when it completes it will -+ /// invoke @c socketReadCallback(). -+ void doRead(); -+ -+ /// @brief Socket read completion callback -+ /// -+ /// Invoked when PingSocket::asyncRead() completes. -+ /// Upon read success and data received: -+ /// -+ /// -# Unpacks the wire data -+ /// -# Pass the resultant ICMPMsg to reply received callback -+ /// -# start next read -+ /// -+ /// On error conditions: -+ /// -+ /// -# Operation aborted: socket is shutting down, simply return -+ /// -# Operation would block/try again: start a new read -+ /// -# Any other error, shut down the channel -+ /// -+ /// @param ec error code indicating either success or the error encountered -+ /// @param length number of bytes read -+ void socketReadCallback(boost::system::error_code ec, size_t length); -+ -+ /// @brief Initiates sending the next ECHO REQUEST -+ /// -+ /// If the channel is able to send (i.e is open, not stopping and not -+ /// currently writing): -+ /// -# Invoke next to send callback to fetch the next target IP address -+ /// -# If there is no next target, return -+ /// -# Construct the ECHO REQUEST for the target and pack it into wire form -+ /// -# Begin sending the request by passing to @c PingSocket::asyncSend() -+ /// -# If the asyncSend() call fails shutdown the channel, otherwise when -+ /// it completes it invokes @c socketWriteCallback(). -+ virtual void sendNext(); -+ -+ /// @brief Socket write completion callback -+ /// -+ /// Invoked when PingSocket::asyncWrite() completes. -+ /// Upon write success: -+ /// -+ /// -# Pass the ECHO REQUEST (i.e. echo_sent) to echo sent callback -+ /// -# start next write -+ /// -+ /// On error conditions: -+ /// -+ /// -# Operation aborted: socket is shutting down, simply return -+ /// -# Operation would block/try again: start a new write -+ /// -# Any other error, shut down the channel -+ /// -+ /// @param echo_sent ECHO REQUEST that was written (or attempted to be -+ /// written) -+ /// @param ec error code indicating either success or the error encountered -+ /// @param length number of bytes written -+ void socketWriteCallback(ICMPMsgPtr echo_sent, boost::system::error_code ec, -+ size_t length); -+ -+ /// @brief Closes the socket channel and invokes the shutdown callback. -+ /// -+ /// This function is invoked to notify the calling layer that the socket -+ /// has encountered an unrecoverable error and is stopping operations. -+ void stopChannel(); -+ -+ /// @brief returns the next unique ECHO instance number. -+ /// -+ /// This method generates and returns the next ECHO instance -+ /// number by incrementing the current value. It is a strictly -+ /// monotonously increasing value beginning at 0x00010001. -+ /// At roll over it resets to 0x00010001. -+ /// -+ /// Must be called in a thread-safe context -+ /// -+ /// @return the next unique instance number. -+ static uint32_t nextEchoInstanceNum(); -+ -+ /// @brief Indicates whether or not a send can be initiated. -+ /// -+ /// Must be called in a thread-safe context -+ /// -+ /// @return True if the socket is open, is not attempting to stop, and is -+ /// not currently sending. -+ bool canSend() { -+ return (socket_ && socket_->isOpen() && !stopping_ && !sending_); -+ } -+ -+ /// @brief Indicates whether or not a read can be initiated. -+ /// -+ /// Must be called in a thread-safe context -+ /// -+ /// @return True if the socket is open, is not attempting to stop, and is -+ /// not currently reading. -+ bool canRead() { -+ return (socket_ && socket_->isOpen() && !stopping_ && !reading_); -+ } -+ -+ /// @brief Returns input buffer size. -+ /// -+ /// Must be called in a thread-safe context -+ /// -+ /// @return size of the input buf -+ size_t getInputBufSize() const; -+ -+ /// @brief Returns pointer to the first byte of the input buffer. -+ /// -+ /// Must be called in a thread-safe context -+ /// -+ /// @return pointer to the data buffer -+ /// @throw InvalidOperation if called when the buffer is empty. -+ unsigned char* getInputBufData(); -+ -+ /// @brief IOService instance the drives socket IO -+ asiolink::IOServicePtr io_service_; -+ -+ /// @brief Callback to invoke to fetch the next address to ping. -+ NextToSendCallback next_to_send_cb_; -+ -+ /// @brief Callback to invoke when an ECHO write has completed. -+ EchoSentCallback echo_sent_cb_; -+ -+ /// @brief Callback to invoke when an ICMP reply has been received. -+ ReplyReceivedCallback reply_received_cb_; -+ -+ /// @brief Callback to invoke when the channel has shutdown. -+ ShutdownCallback shutdown_cb_; -+ -+ /// @brief Socket through which to ping. -+ PingSocketPtr socket_; -+ -+ /// @brief Buffer to hold the contents for most recent socket read. -+ std::vector<uint8_t> input_buf_; -+ -+ /// @brief Retains the endpoint from which the most recent reply was received. -+ ICMPEndpoint reply_endpoint_; -+ -+ /// @brief Indicates whether or not the socket has a read in progress. -+ bool reading_; -+ -+ /// @brief Indicates whether or not the socket has a write in progress. -+ bool sending_; -+ -+ /// @brief Indicates whether or not the channel has been told to stop. -+ bool stopping_; -+ -+ /// @brief The mutex used to protect internal state. -+ const boost::scoped_ptr<std::mutex> mutex_; -+ -+ /// @brief True if channel was opened in single-threaded mode, false -+ /// otherwise. -+ bool single_threaded_; -+ -+ /// @brief Pointer to WatchSocket instance supplying the "select-fd". -+ util::WatchSocketPtr watch_socket_; -+ -+ /// @brief WatchSocket fd registered with IfaceMgr. -+ int registered_write_fd_; -+ -+ /// @brief ICMPSocket fd registered with IfaceMgr. -+ int registered_read_fd_; -+}; -+ -+/// @brief Defines a smart pointer to PingChannel -+typedef boost::shared_ptr<PingChannel> PingChannelPtr; -+ -+} // end of namespace ping_check -+} // end of namespace isc -+ -+#endif -diff --git a/src/hooks/dhcp/ping_check/ping_check.dox b/src/hooks/dhcp/ping_check/ping_check.dox -new file mode 100644 -index 0000000000..a7fbe839c0 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/ping_check.dox -@@ -0,0 +1,44 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+/** -+ -+@mainpage Kea Ping Check Hooks Library -+ -+Welcome to Kea Ping Check Hooks Library. This documentation is -+addressed at developers who are interested in internal operation of the -+library. This file provides information needed to understand and perhaps -+extend this library. -+ -+This documentation is stand-alone: you should have read and -+understood <a href="https://reports.kea.isc.org/dev_guide/">Kea -+Developer's Guide</a> and in particular its section about hooks: <a -+href="https://reports.kea.isc.org/dev_guide/df/d46/hooksdgDevelopersGuide.html"> -+Hooks Developer's Guide</a>. -+ -+@section cbPingCheckOverview Overview -+The @c ping_check hooks library provides the ability for kea-dhcp4 to carry -+out an ICMP ECHO test of a candidate IP address prior to sending that address to -+a DHCPv4 client in a DHCPOFFER message. -+ -+@section cbPingCheckInternals Library Internals -+ -+In addition to the requisite @ref load() and @ref unload() functions, the library -+implements the following callouts: -+ -+- @ref dhcp4_srv_configured() - schedules a (re)start of the ICMP IO layer -+- @ref lease4_offer() - handles requests from kea-dhcp4 core to initiate a ping check -+for a candidate lease -+ -+The load() function instantiates an instance of @ref isc::ping_check::PingCheckMgr. -+This class is the top level object that provides configuration processing and supervises -+the execution of ping checks. -+ -+@section cbPingCheckMTCompatibility Multi-Threading Compatibility -+ -+The @c ping_check hooks library requires multi-threading. -+ -+*/ -diff --git a/src/hooks/dhcp/ping_check/ping_check_callouts.cc b/src/hooks/dhcp/ping_check/ping_check_callouts.cc -new file mode 100644 -index 0000000000..ae006359be ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/ping_check_callouts.cc -@@ -0,0 +1,240 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#include <config.h> -+ -+#include <asiolink/io_service_mgr.h> -+#include <database/audit_entry.h> -+#include <dhcpsrv/cfgmgr.h> -+#include <ping_check_log.h> -+#include <ping_check_mgr.h> -+#include <hooks/hooks.h> -+#include <process/daemon.h> -+#include <string> -+ -+namespace isc { -+namespace ping_check { -+ -+/// @brief PingCheckMgr singleton -+PingCheckMgrPtr mgr; -+ -+} // end of namespace ping_check -+} // end of namespace isc -+ -+using namespace isc; -+using namespace isc::asiolink; -+using namespace isc::log; -+using namespace isc::data; -+using namespace isc::db; -+using namespace isc::dhcp; -+using namespace isc::ping_check; -+using namespace isc::hooks; -+using namespace isc::process; -+using namespace std; -+ -+// Functions accessed by the hooks framework use C linkage to avoid the name -+// mangling that accompanies use of the C++ compiler as well as to avoid -+// issues related to namespaces. -+extern "C" { -+ -+/// @brief dhcp4_srv_configured implementation. -+/// -+/// @param handle callout handle. -+int dhcp4_srv_configured(CalloutHandle& handle) { -+ try { -+ SrvConfigPtr server_config; -+ handle.getArgument("server_config", server_config); -+ mgr->updateSubnetConfig(server_config); -+ -+ NetworkStatePtr network_state; -+ handle.getArgument("network_state", network_state); -+ -+ // Schedule a start of the services. This ensures we begin after -+ // the dust has settled and Kea MT mode has been firmly established. -+ mgr->startService(network_state); -+ IOServiceMgr::instance().registerIOService(mgr->getIOService()); -+ } catch (const std::exception& ex) { -+ LOG_ERROR(ping_check_logger, PING_CHECK_DHCP4_SRV_CONFIGURED_FAILED) -+ .arg(ex.what()); -+ -+ handle.setStatus(isc::hooks::CalloutHandle::NEXT_STEP_DROP); -+ ostringstream os; -+ os << "Error: " << ex.what(); -+ string error(os.str()); -+ handle.setArgument("error", error); -+ return (1); -+ } -+ -+ return (0); -+} -+ -+/// @brief cb4_updated callout implementation. -+/// -+/// If it detects that any subnets were altered by the update it -+/// replaces the subnet cache contents. If any of the subnets -+/// fail to parse, the error is logged and the function returns -+/// a non-zero value. -+/// -+/// @param handle CalloutHandle. -+/// -+/// @return 0 upon success, 1 otherwise -+int cb4_updated(CalloutHandle& handle) { -+ AuditEntryCollectionPtr audit_entries; -+ handle.getArgument("audit_entries", audit_entries); -+ -+ auto const& object_type_idx = audit_entries->get<AuditEntryObjectTypeTag>(); -+ auto range = object_type_idx.equal_range("dhcp4_subnet"); -+ if (std::distance(range.first, range.second)) { -+ try { -+ // Server config has been committed, so use the current configuration. -+ mgr->updateSubnetConfig(CfgMgr::instance().getCurrentCfg()); -+ } catch (const std::exception& ex) { -+ LOG_ERROR(ping_check_logger, PING_CHECK_CB4_UPDATE_FAILED) -+ .arg(ex.what()); -+ return (1); -+ } -+ } -+ -+ return (0); -+} -+ -+/// @brief lease4_offer callout implementation. -+/// -+/// @param handle callout handle. -+int lease4_offer(CalloutHandle& handle) { -+ CalloutHandle::CalloutNextStep status = handle.getStatus(); -+ if (status == CalloutHandle::NEXT_STEP_DROP || -+ status == CalloutHandle::NEXT_STEP_SKIP) { -+ return (0); -+ } -+ -+ Pkt4Ptr query4; -+ Lease4Ptr lease4; -+ ParkingLotHandlePtr parking_lot; -+ try { -+ // Get all arguments available for the leases4_committed hook point. -+ // If any of these arguments is not available this is a programmatic -+ // error. An exception will be thrown which will be caught by the -+ // caller and logged. -+ handle.getArgument("query4", query4); -+ -+ Lease4CollectionPtr leases4; -+ handle.getArgument("leases4", leases4); -+ -+ uint32_t offer_lifetime; -+ handle.getArgument("offer_lifetime", offer_lifetime); -+ -+ Lease4Ptr old_lease; -+ handle.getArgument("old_lease", old_lease); -+ -+ if (query4->getType() != DHCPDISCOVER) { -+ isc_throw(InvalidOperation, "query4 is not a DHCPDISCOVER"); -+ } -+ -+ if (!leases4) { -+ isc_throw(InvalidOperation, "leases4 is null"); -+ } -+ -+ if (!leases4->empty()) { -+ lease4 = (*leases4)[0]; -+ } -+ -+ if (!lease4) { -+ isc_throw(InvalidOperation, "leases4 is empty, no lease to check"); -+ } -+ -+ // Fetch the parking lot. If it's empty the server is not employing -+ // parking, which is fine. -+ // Create a reference to the parked packet. This signals that we have a -+ // stake in unparking it. -+ parking_lot = handle.getParkingLotHandlePtr(); -+ if (parking_lot) { -+ parking_lot->reference(query4); -+ } -+ -+ // Get configuration based on the lease's subnet. -+ auto const& config = mgr->getScopedConfig(lease4); -+ -+ // Call shouldPing() to determine if we should ping check or not. -+ // - status == PARK - ping check it -+ // - status == CONTINUE - check not needed, release DHCPOFFER to client -+ // - status == DROP - duplicate check, drop the duplicate DHCPOFFER -+ status = mgr->shouldPing(lease4, query4, old_lease, config); -+ handle.setStatus(status); -+ if (status == CalloutHandle::NEXT_STEP_PARK) { -+ mgr->startPing(lease4, query4, parking_lot, config); -+ } else { -+ // Dereference the parked packet. This releases our stake in it. -+ if (parking_lot) { -+ parking_lot->dereference(query4); -+ } -+ } -+ -+ } catch (const std::exception& ex) { -+ LOG_ERROR(ping_check_logger, PING_CHECK_LEASE4_OFFER_FAILED) -+ .arg(query4 ? query4->getLabel() : "<no query>") -+ .arg(lease4 ? lease4->addr_.toText() : "<no lease>") -+ .arg(ex.what()); -+ // Make sure we dereference. -+ if (parking_lot) { -+ parking_lot->dereference(query4); -+ } -+ -+ return (1); -+ } -+ -+ return (0); -+} -+ -+/// @brief This function is called when the library is loaded. -+/// -+/// @param handle library handle -+/// @return 0 when initialization is successful, 1 otherwise -+int load(LibraryHandle& handle) { -+ try { -+ // Make the hook library only loadable by kea-dhcp4. -+ const string& proc_name = Daemon::getProcName(); -+ if (proc_name != "kea-dhcp4") { -+ isc_throw(isc::Unexpected, "Bad process name: " << proc_name -+ << ", expected kea-dhcp4"); -+ } -+ -+ // Instantiate the manager singleton. -+ mgr.reset(new PingCheckMgr()); -+ -+ // Configure the manager using the hook library's parameters. -+ ConstElementPtr json = handle.getParameters(); -+ mgr->configure(json); -+ } catch (const exception& ex) { -+ LOG_ERROR(ping_check_logger, PING_CHECK_LOAD_ERROR) -+ .arg(ex.what()); -+ return (1); -+ } -+ -+ LOG_INFO(ping_check_logger, PING_CHECK_LOAD_OK); -+ return (0); -+} -+ -+/// @brief This function is called when the library is unloaded. -+/// -+/// @return always 0. -+int unload() { -+ if (mgr) { -+ IOServiceMgr::instance().unregisterIOService(mgr->getIOService()); -+ mgr.reset(); -+ } -+ LOG_INFO(ping_check_logger, PING_CHECK_UNLOAD); -+ return (0); -+} -+ -+/// @brief This function is called to retrieve the multi-threading compatibility. -+/// -+/// @return 1 which means compatible with multi-threading. -+int multi_threading_compatible() { -+ return (1); -+} -+ -+} // end extern "C" -diff --git a/src/hooks/dhcp/ping_check/ping_check_config.cc b/src/hooks/dhcp/ping_check/ping_check_config.cc -new file mode 100644 -index 0000000000..a1c69da61e ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/ping_check_config.cc -@@ -0,0 +1,98 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#include <config.h> -+ -+#include <ping_check_config.h> -+ -+using namespace isc; -+using namespace isc::data; -+using namespace isc::dhcp; -+ -+namespace isc { -+namespace ping_check { -+ -+const data::SimpleKeywords -+PingCheckConfig::CONFIG_KEYWORDS = -+{ -+ { "enable-ping-check", Element::boolean }, -+ { "min-ping-requests", Element::integer }, -+ { "reply-timeout", Element::integer }, -+ { "ping-cltt-secs", Element::integer}, -+ { "ping-channel-threads", Element::integer} -+}; -+ -+PingCheckConfig::PingCheckConfig() : -+ enable_ping_check_(true), -+ min_ping_requests_(1), -+ reply_timeout_(100), -+ ping_cltt_secs_(60), -+ ping_channel_threads_(0) { -+} -+ -+void -+PingCheckConfig::parse(data::ConstElementPtr config) { -+ // Use a local instance to collect values. This way we -+ // avoid corrupting current values if there are any errors. -+ PingCheckConfig local; -+ -+ // Note checkKeywords() will throw DhcpConfigError if there is a problem. -+ SimpleParser::checkKeywords(CONFIG_KEYWORDS, config); -+ ConstElementPtr value = config->get("enable-ping-check"); -+ if (value) { -+ local.setEnablePingCheck(value->boolValue()); -+ } -+ -+ value = config->get("min-ping-requests"); -+ if (value) { -+ int64_t val = value->intValue(); -+ if (val <= 0) { -+ isc_throw(DhcpConfigError, "invalid min-ping-requests: '" -+ << val << "', must be greater than 0"); -+ } -+ -+ local.setMinPingRequests(static_cast<size_t>(val)); -+ } -+ -+ value = config->get("reply-timeout"); -+ if (value) { -+ int64_t val = value->intValue(); -+ if (val <= 0) { -+ isc_throw(DhcpConfigError, "invalid reply-timeout: '" -+ << val << "', must be greater than 0"); -+ } -+ -+ local.setReplyTimeout(static_cast<size_t>(val)); -+ } -+ -+ value = config->get("ping-cltt-secs"); -+ if (value) { -+ int64_t val = value->intValue(); -+ if (val < 0) { -+ isc_throw(DhcpConfigError, "invalid ping-cltt-secs: '" -+ << val << "', cannot be less than 0"); -+ } -+ -+ local.setPingClttSecs(static_cast<size_t>(val)); -+ } -+ -+ value = config->get("ping-channel-threads"); -+ if (value) { -+ int64_t val = value->intValue(); -+ if (val < 0) { -+ isc_throw(DhcpConfigError, "invalid ping-channel-threads: '" -+ << val << "', cannot be less than 0"); -+ } -+ -+ local.setPingChannelThreads(static_cast<size_t>(val)); -+ } -+ -+ // All values good, copy from local instance. -+ *this = local; -+} -+ -+} // end of namespace ping_check -+} // end of namespace isc -diff --git a/src/hooks/dhcp/ping_check/ping_check_config.h b/src/hooks/dhcp/ping_check/ping_check_config.h -new file mode 100644 -index 0000000000..9fd23eba59 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/ping_check_config.h -@@ -0,0 +1,134 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#ifndef PING_CHECK_CONFIG_H -+#define PING_CHECK_CONFIG_H -+ -+#include <cc/data.h> -+#include <cc/simple_parser.h> -+ -+namespace isc { -+namespace ping_check { -+ -+/// @brief Houses the Ping check configuration parameters for a single scope -+/// (e.g. global, subnet...); -+class PingCheckConfig { -+public: -+ /// @brief List of valid parameters and expected types. -+ static const data::SimpleKeywords CONFIG_KEYWORDS; -+ -+ /// @brief Constructor -+ PingCheckConfig(); -+ -+ /// @brief Destructor -+ ~PingCheckConfig() = default; -+ -+ /// @brief Extracts member values from an Element::map -+ /// -+ /// @param config map of configuration parameters -+ /// -+ /// @throw BadValue if invalid values are detected. -+ void parse(data::ConstElementPtr config); -+ -+ /// @brief Fetches the value of enable-ping-check -+ /// -+ /// @return boolean value of enable-ping-check -+ bool getEnablePingCheck() const { -+ return (enable_ping_check_); -+ }; -+ -+ /// @brief Sets the value of enable-ping-check -+ /// -+ /// @param value new value for enable-ping-check -+ void setEnablePingCheck(bool value) { -+ enable_ping_check_ = value; -+ } -+ -+ /// @brief Fetches the value of min-ping-requests -+ /// -+ /// @return integer value of min-ping-requests -+ uint32_t getMinPingRequests() const { -+ return (min_ping_requests_); -+ }; -+ -+ /// @brief Sets the value of min-ping-requests -+ /// -+ /// @param value new value for min-ping-requests -+ void setMinPingRequests(uint32_t value) { -+ min_ping_requests_ = value; -+ } -+ -+ /// @brief Fetches the value of reply-timeout -+ /// -+ /// @return integer value of reply-timeout -+ uint32_t getReplyTimeout() const { -+ return (reply_timeout_); -+ } -+ -+ /// @brief Sets the value of reply-timeout -+ /// -+ /// @param value new value for reply-timeout -+ void setReplyTimeout(uint32_t value) { -+ reply_timeout_ = value; -+ } -+ -+ /// @brief Fetches the value of ping-cltt-secs -+ /// -+ /// @return integer value of ping-cltt-secs -+ uint32_t getPingClttSecs() const { -+ return (ping_cltt_secs_); -+ } -+ -+ /// @brief Sets the value of ping-cltt-secs -+ /// -+ /// @param value new value for ping-cltt-secs -+ void setPingClttSecs(uint32_t value) { -+ ping_cltt_secs_ = value; -+ } -+ -+ /// @brief Fetches the value of ping-channel-threads -+ /// -+ /// @return integer value of ping-channel-threads -+ uint32_t getPingChannelThreads() const { -+ return (ping_channel_threads_); -+ } -+ -+ /// @brief Sets the value of ping-channel-threads -+ /// -+ /// @param value new value for ping-channel-threads -+ void setPingChannelThreads(uint32_t value) { -+ ping_channel_threads_ = value; -+ } -+ -+private: -+ // @brief True if checking is enabled. -+ bool enable_ping_check_; -+ -+ /// @brief minimum number of ECHO REQUESTs sent, without replies received, -+ /// required to declare an address free to offer. -+ uint32_t min_ping_requests_; -+ -+ /// @brief maximum number of milliseconds to wait for an ECHO REPLY after -+ /// an ECHO REQUEST has been sent. -+ uint32_t reply_timeout_; -+ -+ /// @brief minimum number of seconds that must elapse after the lease's CLTT -+ /// before a ping check will be conducted, when the client is the lease's -+ /// previous owner. -+ uint32_t ping_cltt_secs_; -+ -+ /// @brief Number of threads to use if Kea core is multi-threaded. -+ /// Defaults to 0 (for now) which means follow core number of threads. -+ size_t ping_channel_threads_; -+}; -+ -+/// @brief Defines a shared pointer to a PingCheckConfig. -+typedef boost::shared_ptr<PingCheckConfig> PingCheckConfigPtr; -+ -+} // end of namespace ping_check -+} // end of namespace isc -+ -+#endif -diff --git a/src/hooks/dhcp/ping_check/ping_check_log.cc b/src/hooks/dhcp/ping_check/ping_check_log.cc -new file mode 100644 -index 0000000000..9e877ff9b5 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/ping_check_log.cc -@@ -0,0 +1,17 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#include <config.h> -+ -+#include <ping_check_log.h> -+ -+namespace isc { -+namespace ping_check { -+ -+isc::log::Logger ping_check_logger("ping-check-hooks"); -+ -+} // namespace ping_check -+} // namespace isc -diff --git a/src/hooks/dhcp/ping_check/ping_check_log.h b/src/hooks/dhcp/ping_check/ping_check_log.h -new file mode 100644 -index 0000000000..22e0fca953 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/ping_check_log.h -@@ -0,0 +1,23 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#ifndef PING_CHECK_LOG_H -+#define PING_CHECK_LOG_H -+ -+#include <log/logger_support.h> -+#include <log/macros.h> -+#include <log/log_dbglevels.h> -+#include <ping_check_messages.h> -+#include <iostream> -+ -+namespace isc { -+namespace ping_check { -+ -+extern isc::log::Logger ping_check_logger; -+ -+} // end of namespace ping_check -+} // end of namespace isc -+#endif -diff --git a/src/hooks/dhcp/ping_check/ping_check_messages.cc b/src/hooks/dhcp/ping_check/ping_check_messages.cc -new file mode 100644 -index 0000000000..7dea2c2397 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/ping_check_messages.cc -@@ -0,0 +1,99 @@ -+// File created from src/hooks/dhcp/ping_check/ping_check_messages.mes -+ -+#include <cstddef> -+#include <log/message_types.h> -+#include <log/message_initializer.h> -+ -+extern const isc::log::MessageID PING_CHECK_CB4_UPDATE_FAILED = "PING_CHECK_CB4_UPDATE_FAILED"; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_ECHO_REPLY_RECEIVED = "PING_CHECK_CHANNEL_ECHO_REPLY_RECEIVED"; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_ECHO_REQUEST_SENT = "PING_CHECK_CHANNEL_ECHO_REQUEST_SENT"; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_MALFORMED_PACKET_RECEIVED = "PING_CHECK_CHANNEL_MALFORMED_PACKET_RECEIVED"; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_NETWORK_WRITE_ERROR = "PING_CHECK_CHANNEL_NETWORK_WRITE_ERROR"; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_CLOSED = "PING_CHECK_CHANNEL_SOCKET_CLOSED"; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_CLOSE_ERROR = "PING_CHECK_CHANNEL_SOCKET_CLOSE_ERROR"; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_OPENED = "PING_CHECK_CHANNEL_SOCKET_OPENED"; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_READ_FAILED = "PING_CHECK_CHANNEL_SOCKET_READ_FAILED"; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_WRITE_FAILED = "PING_CHECK_CHANNEL_SOCKET_WRITE_FAILED"; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_STOP = "PING_CHECK_CHANNEL_STOP"; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_WATCH_SOCKET_CLEAR_ERROR = "PING_CHECK_CHANNEL_WATCH_SOCKET_CLEAR_ERROR"; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_WATCH_SOCKET_CLOSE_ERROR = "PING_CHECK_CHANNEL_WATCH_SOCKET_CLOSE_ERROR"; -+extern const isc::log::MessageID PING_CHECK_DHCP4_SRV_CONFIGURED_FAILED = "PING_CHECK_DHCP4_SRV_CONFIGURED_FAILED"; -+extern const isc::log::MessageID PING_CHECK_DUPLICATE_CHECK = "PING_CHECK_DUPLICATE_CHECK"; -+extern const isc::log::MessageID PING_CHECK_LEASE4_OFFER_FAILED = "PING_CHECK_LEASE4_OFFER_FAILED"; -+extern const isc::log::MessageID PING_CHECK_LOAD_ERROR = "PING_CHECK_LOAD_ERROR"; -+extern const isc::log::MessageID PING_CHECK_LOAD_OK = "PING_CHECK_LOAD_OK"; -+extern const isc::log::MessageID PING_CHECK_MGR_CHANNEL_DOWN = "PING_CHECK_MGR_CHANNEL_DOWN"; -+extern const isc::log::MessageID PING_CHECK_MGR_LEASE_FREE_TO_USE = "PING_CHECK_MGR_LEASE_FREE_TO_USE"; -+extern const isc::log::MessageID PING_CHECK_MGR_NEXT_ECHO_SCHEDULED = "PING_CHECK_MGR_NEXT_ECHO_SCHEDULED"; -+extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_ECHO_REPLY = "PING_CHECK_MGR_RECEIVED_ECHO_REPLY"; -+extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_UNEXPECTED_ECHO_REPLY = "PING_CHECK_MGR_RECEIVED_UNEXPECTED_ECHO_REPLY"; -+extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_UNEXPECTED_UNREACHABLE_MSG = "PING_CHECK_MGR_RECEIVED_UNEXPECTED_UNREACHABLE_MSG"; -+extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_UNREACHABLE_MSG = "PING_CHECK_MGR_RECEIVED_UNREACHABLE_MSG"; -+extern const isc::log::MessageID PING_CHECK_MGR_REPLY_RECEIVED_ERROR = "PING_CHECK_MGR_REPLY_RECEIVED_ERROR"; -+extern const isc::log::MessageID PING_CHECK_MGR_REPLY_TIMEOUT_EXPIRED = "PING_CHECK_MGR_REPLY_TIMEOUT_EXPIRED"; -+extern const isc::log::MessageID PING_CHECK_MGR_SEND_COMPLETED_ERROR = "PING_CHECK_MGR_SEND_COMPLETED_ERROR"; -+extern const isc::log::MessageID PING_CHECK_MGR_STARTED = "PING_CHECK_MGR_STARTED"; -+extern const isc::log::MessageID PING_CHECK_MGR_STARTED_SINGLE_THREADED = "PING_CHECK_MGR_STARTED_SINGLE_THREADED"; -+extern const isc::log::MessageID PING_CHECK_MGR_START_PING_CHECK = "PING_CHECK_MGR_START_PING_CHECK"; -+extern const isc::log::MessageID PING_CHECK_MGR_STOPPED = "PING_CHECK_MGR_STOPPED"; -+extern const isc::log::MessageID PING_CHECK_MGR_STOPPING = "PING_CHECK_MGR_STOPPING"; -+extern const isc::log::MessageID PING_CHECK_MGR_SUBNET_CONFIG_FAILED = "PING_CHECK_MGR_SUBNET_CONFIG_FAILED"; -+extern const isc::log::MessageID PING_CHECK_PAUSE_FAILED = "PING_CHECK_PAUSE_FAILED"; -+extern const isc::log::MessageID PING_CHECK_PAUSE_ILLEGAL = "PING_CHECK_PAUSE_ILLEGAL"; -+extern const isc::log::MessageID PING_CHECK_PAUSE_PERMISSIONS_FAILED = "PING_CHECK_PAUSE_PERMISSIONS_FAILED"; -+extern const isc::log::MessageID PING_CHECK_RESUME_FAILED = "PING_CHECK_RESUME_FAILED"; -+extern const isc::log::MessageID PING_CHECK_UNEXPECTED_READ_ERROR = "PING_CHECK_UNEXPECTED_READ_ERROR"; -+extern const isc::log::MessageID PING_CHECK_UNEXPECTED_WRITE_ERROR = "PING_CHECK_UNEXPECTED_WRITE_ERROR"; -+extern const isc::log::MessageID PING_CHECK_UNLOAD = "PING_CHECK_UNLOAD"; -+ -+namespace { -+ -+const char* values[] = { -+ "PING_CHECK_CB4_UPDATE_FAILED", "A subnet ping-check parameters failed to parse after being updated %1", -+ "PING_CHECK_CHANNEL_ECHO_REPLY_RECEIVED", "from address %1, id %2, sequence %3", -+ "PING_CHECK_CHANNEL_ECHO_REQUEST_SENT", "to address %1, id %2, sequence %3", -+ "PING_CHECK_CHANNEL_MALFORMED_PACKET_RECEIVED", "error occurred unpacking message %1, discarding it", -+ "PING_CHECK_CHANNEL_NETWORK_WRITE_ERROR", "occurred trying to ping %1, error %2", -+ "PING_CHECK_CHANNEL_SOCKET_CLOSED", "ICMP socket has been closed.", -+ "PING_CHECK_CHANNEL_SOCKET_CLOSE_ERROR", "an attempt to close the ICMP socket failed %1", -+ "PING_CHECK_CHANNEL_SOCKET_OPENED", "ICMP socket been opened successfully.", -+ "PING_CHECK_CHANNEL_SOCKET_READ_FAILED", "socket read completed with an error %1", -+ "PING_CHECK_CHANNEL_SOCKET_WRITE_FAILED", "socket write completed with an error %1", -+ "PING_CHECK_CHANNEL_STOP", "channel is stopping operations.", -+ "PING_CHECK_CHANNEL_WATCH_SOCKET_CLEAR_ERROR", "an attempt to clear the WatchSocket associated with", -+ "PING_CHECK_CHANNEL_WATCH_SOCKET_CLOSE_ERROR", "an attempt to close the WatchSocket associated with", -+ "PING_CHECK_DHCP4_SRV_CONFIGURED_FAILED", "dhcp4_srv_configured callout failed %1", -+ "PING_CHECK_DUPLICATE_CHECK", "Ping check already in progress for %1, initiated by %2", -+ "PING_CHECK_LEASE4_OFFER_FAILED", "lease4_offer callout failed for query %1, lease address %2, reason %3", -+ "PING_CHECK_LOAD_ERROR", "loading Ping Check hooks library failed %1", -+ "PING_CHECK_LOAD_OK", "Ping Check hooks library loaded successfully.", -+ "PING_CHECK_MGR_CHANNEL_DOWN", "Ping Channel has shutdown, ping checking will be skipped", -+ "PING_CHECK_MGR_LEASE_FREE_TO_USE", "address %1 is free to use for %2", -+ "PING_CHECK_MGR_NEXT_ECHO_SCHEDULED", "for %1, scheduling ECHO_REQUEST %2 of %3", -+ "PING_CHECK_MGR_RECEIVED_ECHO_REPLY", "from %1, id %2, sequence %3", -+ "PING_CHECK_MGR_RECEIVED_UNEXPECTED_ECHO_REPLY", "from %1, id %2, sequence %3 received after reply-timeout expired", -+ "PING_CHECK_MGR_RECEIVED_UNEXPECTED_UNREACHABLE_MSG", "for %1, id %2, sequence %3 received after reply-timeout expired", -+ "PING_CHECK_MGR_RECEIVED_UNREACHABLE_MSG", "for %1, id %2, sequence %3", -+ "PING_CHECK_MGR_REPLY_RECEIVED_ERROR", "an error occurred processing an ICMP reply message %1", -+ "PING_CHECK_MGR_REPLY_TIMEOUT_EXPIRED", "for %1, ECHO REQUEST %2 of %3, reply-timeout %4", -+ "PING_CHECK_MGR_SEND_COMPLETED_ERROR", "an error occurred in the send completion callback %1", -+ "PING_CHECK_MGR_STARTED", "ping channel operations are running, number of threads %1", -+ "PING_CHECK_MGR_STARTED_SINGLE_THREADED", "single-threaded ping channel operations are running", -+ "PING_CHECK_MGR_START_PING_CHECK", "for %1, initiated by %2", -+ "PING_CHECK_MGR_STOPPED", "channel operations have stopped", -+ "PING_CHECK_MGR_STOPPING", "ping channel operations are stopping", -+ "PING_CHECK_MGR_SUBNET_CONFIG_FAILED", "user-context for subnet id %1, contains invalid ping-check %2", -+ "PING_CHECK_PAUSE_FAILED", "Pausing ping channel operations failed %1", -+ "PING_CHECK_PAUSE_ILLEGAL", "Pausing ping channel operations not allowed %1", -+ "PING_CHECK_PAUSE_PERMISSIONS_FAILED", "Permissions check for ping-channel pause failed %1", -+ "PING_CHECK_RESUME_FAILED", "Resuming ping channel operations failed %1", -+ "PING_CHECK_UNEXPECTED_READ_ERROR", "could not start next socket read %1", -+ "PING_CHECK_UNEXPECTED_WRITE_ERROR", "could not start next socket write %1", -+ "PING_CHECK_UNLOAD", "Ping Check hooks library has been unloaded", -+ NULL -+}; -+ -+const isc::log::MessageInitializer initializer(values); -+ -+} // Anonymous namespace -+ -diff --git a/src/hooks/dhcp/ping_check/ping_check_messages.h b/src/hooks/dhcp/ping_check/ping_check_messages.h -new file mode 100644 -index 0000000000..9326c699e8 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/ping_check_messages.h -@@ -0,0 +1,50 @@ -+// File created from src/hooks/dhcp/ping_check/ping_check_messages.mes -+ -+#ifndef PING_CHECK_MESSAGES_H -+#define PING_CHECK_MESSAGES_H -+ -+#include <log/message_types.h> -+ -+extern const isc::log::MessageID PING_CHECK_CB4_UPDATE_FAILED; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_ECHO_REPLY_RECEIVED; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_ECHO_REQUEST_SENT; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_MALFORMED_PACKET_RECEIVED; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_NETWORK_WRITE_ERROR; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_CLOSED; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_CLOSE_ERROR; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_OPENED; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_READ_FAILED; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_WRITE_FAILED; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_STOP; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_WATCH_SOCKET_CLEAR_ERROR; -+extern const isc::log::MessageID PING_CHECK_CHANNEL_WATCH_SOCKET_CLOSE_ERROR; -+extern const isc::log::MessageID PING_CHECK_DHCP4_SRV_CONFIGURED_FAILED; -+extern const isc::log::MessageID PING_CHECK_DUPLICATE_CHECK; -+extern const isc::log::MessageID PING_CHECK_LEASE4_OFFER_FAILED; -+extern const isc::log::MessageID PING_CHECK_LOAD_ERROR; -+extern const isc::log::MessageID PING_CHECK_LOAD_OK; -+extern const isc::log::MessageID PING_CHECK_MGR_CHANNEL_DOWN; -+extern const isc::log::MessageID PING_CHECK_MGR_LEASE_FREE_TO_USE; -+extern const isc::log::MessageID PING_CHECK_MGR_NEXT_ECHO_SCHEDULED; -+extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_ECHO_REPLY; -+extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_UNEXPECTED_ECHO_REPLY; -+extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_UNEXPECTED_UNREACHABLE_MSG; -+extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_UNREACHABLE_MSG; -+extern const isc::log::MessageID PING_CHECK_MGR_REPLY_RECEIVED_ERROR; -+extern const isc::log::MessageID PING_CHECK_MGR_REPLY_TIMEOUT_EXPIRED; -+extern const isc::log::MessageID PING_CHECK_MGR_SEND_COMPLETED_ERROR; -+extern const isc::log::MessageID PING_CHECK_MGR_STARTED; -+extern const isc::log::MessageID PING_CHECK_MGR_STARTED_SINGLE_THREADED; -+extern const isc::log::MessageID PING_CHECK_MGR_START_PING_CHECK; -+extern const isc::log::MessageID PING_CHECK_MGR_STOPPED; -+extern const isc::log::MessageID PING_CHECK_MGR_STOPPING; -+extern const isc::log::MessageID PING_CHECK_MGR_SUBNET_CONFIG_FAILED; -+extern const isc::log::MessageID PING_CHECK_PAUSE_FAILED; -+extern const isc::log::MessageID PING_CHECK_PAUSE_ILLEGAL; -+extern const isc::log::MessageID PING_CHECK_PAUSE_PERMISSIONS_FAILED; -+extern const isc::log::MessageID PING_CHECK_RESUME_FAILED; -+extern const isc::log::MessageID PING_CHECK_UNEXPECTED_READ_ERROR; -+extern const isc::log::MessageID PING_CHECK_UNEXPECTED_WRITE_ERROR; -+extern const isc::log::MessageID PING_CHECK_UNLOAD; -+ -+#endif // PING_CHECK_MESSAGES_H -diff --git a/src/hooks/dhcp/ping_check/ping_check_messages.mes b/src/hooks/dhcp/ping_check/ping_check_messages.mes -new file mode 100644 -index 0000000000..21d407bedf ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/ping_check_messages.mes -@@ -0,0 +1,229 @@ -+# Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+# -+# This Source Code Form is subject to the terms of the Mozilla Public -+# License, v. 2.0. If a copy of the MPL was not distributed with this -+# file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+% PING_CHECK_CB4_UPDATE_FAILED A subnet ping-check parameters failed to parse after being updated %1 -+This error message is emitted when an error occurs trying to parse a subnet -+ping-check parameters after the subnet was updated via configuration backend. -+This implies one or more of the parameters is invalid and must be corrected. -+ -+% PING_CHECK_CHANNEL_ECHO_REPLY_RECEIVED from address %1, id %2, sequence %3 -+Logged at debug log level 50. -+This debug message is issued when an ECHO REPLY has been received on -+the ping channel's ICMP socket. -+ -+% PING_CHECK_CHANNEL_ECHO_REQUEST_SENT to address %1, id %2, sequence %3 -+Logged at debug log level 50. -+This debug message is issued when an ECHO REQUEST has been written to the -+ping channel's ICMP socket. -+ -+% PING_CHECK_CHANNEL_MALFORMED_PACKET_RECEIVED error occurred unpacking message %1, discarding it -+Logged at debug log level 40. -+This debug message is emitted when an ICMP packet has been received -+that could not be unpacked. -+ -+% PING_CHECK_CHANNEL_NETWORK_WRITE_ERROR occurred trying to ping %1, error %2 -+This error message occurs when an asynchronous write on the ICMP socket -+failed trying to send on the ping target's network. This may mean an interface -+is down or there is a configuration error. The lease address to ping and the -+type of the error are provided in the arguments. -+ -+% PING_CHECK_CHANNEL_SOCKET_CLOSED ICMP socket has been closed. -+Logged at debug log level 40. -+This debug message is emitted when the ICMP socket for carrying out -+ping checks has been closed. -+ -+% PING_CHECK_CHANNEL_SOCKET_CLOSE_ERROR an attempt to close the ICMP socket failed %1 -+This error message is emitted when an unexpected error occurred -+while closing the ping check ICMP socket. The error detail is -+provided as an argument of the log message. -+ -+% PING_CHECK_CHANNEL_SOCKET_OPENED ICMP socket been opened successfully. -+Logged at debug log level 40. -+This debug message is emitted when the ICMP socket for carrying out -+ping checks has been successfully opened. -+ -+% PING_CHECK_CHANNEL_SOCKET_READ_FAILED socket read completed with an error %1 -+This error message occurs when an asynchronous read on the ICMP socket -+failed. The details of the error are provided as an argument of the log -+message. -+ -+% PING_CHECK_CHANNEL_SOCKET_WRITE_FAILED socket write completed with an error %1 -+This error message occurs when an asynchronous write on the ICMP socket -+failed. The details of the error are provided as an argument of the log -+message. -+ -+% PING_CHECK_CHANNEL_STOP channel is stopping operations. -+Logged at debug log level 40. -+This debug message indicates that the channel is stopping operations and -+closing the ICMP socket. The reason for stopping should be apparent in -+preceding log messages. -+ -+% PING_CHECK_CHANNEL_WATCH_SOCKET_CLEAR_ERROR an attempt to clear the WatchSocket associated with -+the single-threaded ping-channel failed %1 -+This error message is emitted when an unexpected error occurred -+while clearing the ready marker of the WatchSocket associated with -+the ping check channel. This can only occur when running in -+single-threaded mode. The error detail is provided as an argument -+of the log message. -+ -+% PING_CHECK_CHANNEL_WATCH_SOCKET_CLOSE_ERROR an attempt to close the WatchSocket associated with -+the single-threaded ping-channel failed %1 -+This error message is emitted when an unexpected error occurred -+while closing the WatchSocket associated with the ping check channel. -+This can only occur when running in single-threaded mode. -+The error detail is provided as an argument of the log message. -+ -+% PING_CHECK_DHCP4_SRV_CONFIGURED_FAILED dhcp4_srv_configured callout failed %1 -+This error message indicates an error during the Ping Check hook -+library dhcp4_srv_configured callout. The details of the error are -+provided as argument of the log message. -+ -+% PING_CHECK_DUPLICATE_CHECK Ping check already in progress for %1, initiated by %2 -+Logged at debug log level 40. -+This debug message is emitted when a duplicate request to test an address -+is received. When this occurs the duplicate test will be skipped and -+the associated DHCPOFFER will be dropped. -+ -+% PING_CHECK_LEASE4_OFFER_FAILED lease4_offer callout failed for query %1, lease address %2, reason %3 -+This error message indicates an error during the Ping Check hook -+library lease4_offer callout. The details of the error are -+provided as argument of the log message. -+ -+% PING_CHECK_LOAD_ERROR loading Ping Check hooks library failed %1 -+This error message indicates an error during loading the Ping Check -+hooks library. The details of the error are provided as argument of -+the log message. -+ -+% PING_CHECK_LOAD_OK Ping Check hooks library loaded successfully. -+This info message indicates that the Ping Check hooks library has -+been loaded successfully. -+ -+% PING_CHECK_MGR_CHANNEL_DOWN Ping Channel has shutdown, ping checking will be skipped -+This error message is emitted when the underlying ICMP channel -+has stopped due to an unrecoverable error. DHCP service may continue -+to function but without performing ping checks. Prior log messages should -+provide details. -+ -+% PING_CHECK_MGR_LEASE_FREE_TO_USE address %1 is free to use for %2 -+Logged at debug log level 40. -+This debug message is emitted when ping check has deemed an -+address is free to use. The log arguments detail the lease address -+checked and the query which initiated the check. -+ -+% PING_CHECK_MGR_NEXT_ECHO_SCHEDULED for %1, scheduling ECHO_REQUEST %2 of %3 -+Logged at debug log level 50. -+This debug message is emitted when the minimum number of ECHO REQUESTs -+is greater than 1 and the next ECHO REQUEST for a given lease address has -+been scheduled. -+ -+% PING_CHECK_MGR_RECEIVED_ECHO_REPLY from %1, id %2, sequence %3 -+Logged at debug log level 40. -+This debug message is emitted when an ECHO REPLY message has been received. -+The log argument details the source IP address, id, and sequence number of -+the ECHO REPLY. -+ -+% PING_CHECK_MGR_RECEIVED_UNEXPECTED_ECHO_REPLY from %1, id %2, sequence %3 received after reply-timeout expired -+Logged at debug log level 50. -+This debug message is emitted when an ECHO REPLY has been received after the -+reply-timeout has expired and is no longer of interest. This may be an errant -+ECHO REPLY or it may indicate that the reply-timeout value is too short. The -+log argument details the source IP address, id, and sequence number of the reply. -+ -+% PING_CHECK_MGR_RECEIVED_UNEXPECTED_UNREACHABLE_MSG for %1, id %2, sequence %3 received after reply-timeout expired -+Logged at debug log level 50. -+This debug message is emitted when an UNREACHABLE message has been received -+after the reply-timeout has expired and is no longer of interest. This may -+be an errant message or it may indicate that the reply-timeout value is -+too short. -+ -+% PING_CHECK_MGR_RECEIVED_UNREACHABLE_MSG for %1, id %2, sequence %3 -+Logged at debug log level 50. -+This debug message is emitted when an UNREACHABLE message has been received. -+The log argument details the target IP address, id, and sequence number from -+the embedded ECHO REQUEST. -+ -+% PING_CHECK_MGR_REPLY_RECEIVED_ERROR an error occurred processing an ICMP reply message %1 -+This debug message is emitted when an error occurred while processing an inbound -+ICMP message. The log argument describes the specific error. -+ -+% PING_CHECK_MGR_REPLY_TIMEOUT_EXPIRED for %1, ECHO REQUEST %2 of %3, reply-timeout %4 -+Logged at debug log level 50. -+This debug message is emitted when no reply is received to an -+ECHO REQUEST before the configured timeout value, `reply-timeout` -+was reached. The log arguments provides details. -+ -+% PING_CHECK_MGR_SEND_COMPLETED_ERROR an error occurred in the send completion callback %1 -+This error message is emitted when an unexpected error occurred after the completion of -+a successful write to the PingChannel socket. The log argument describes the -+specific error. -+ -+% PING_CHECK_MGR_STARTED ping channel operations are running, number of threads %1 -+This message is emitted when the ping check channel has been opened -+and is ready to process requests. The log argument includes the number of -+threads in the channel's thread pool. -+ -+% PING_CHECK_MGR_STARTED_SINGLE_THREADED single-threaded ping channel operations are running -+This message is emitted when the ping check channel has been opened -+and is ready to process requests in single-threaded mode. -+ -+% PING_CHECK_MGR_START_PING_CHECK for %1, initiated by %2 -+Logged at debug log level 40. -+This debug message is emitted when a ping check for an address -+has been initiated. The log arguments detail the lease address to -+ping and the query which initiated the check. -+ -+% PING_CHECK_MGR_STOPPED channel operations have stopped -+This message is emitted when the ping check channel operations -+have been stopped. -+ -+% PING_CHECK_MGR_STOPPING ping channel operations are stopping -+Logged at debug log level 40. -+This debug message is emitted when the ping check channel is stopping -+operations, typically due to configuration event or server shutdown. -+ -+% PING_CHECK_MGR_SUBNET_CONFIG_FAILED user-context for subnet id %1, contains invalid ping-check %2 -+This error message indicates that a subnet was updated via subnet commands -+and its 'user-context' contains invalid 'ping-check' configuration. The -+server will log the error once and then use global ping-check parameters -+for the subnet until the configuration is corrected. -+ -+% PING_CHECK_PAUSE_FAILED Pausing ping channel operations failed %1 -+This error message is emitted when an unexpected error occurred while -+attempting to pause the ping channel's thread pool. This error is highly -+unlikely and indicates a programmatic issue that should be reported as -+defect. -+ -+% PING_CHECK_PAUSE_ILLEGAL Pausing ping channel operations not allowed %1 -+This error message is emitted when attempting to pause the ping channel's -+thread pool. This indicates that a channel thread attempted to use a critical -+section which would result in a dead-lock. This error is highly unlikely -+and indicates a programmatic issue that should be reported as a defect. -+ -+% PING_CHECK_PAUSE_PERMISSIONS_FAILED Permissions check for ping-channel pause failed %1 -+This error message is emitted when an unexpected error occurred while -+validating an attempt to pause the ping channel's thread pool. This error -+is highly unlikely and indicates a programmatic issue that should be -+reported as a defect. -+ -+% PING_CHECK_RESUME_FAILED Resuming ping channel operations failed %1 -+This error message is emitted when an unexpected error occurred while -+attempting to resume operation of the ping channel's thread pool. This -+error is highly unlikely and indicates a programmatic issue that should -+be reported as defect. -+ -+% PING_CHECK_UNEXPECTED_READ_ERROR could not start next socket read %1 -+This error message occurs when initiating an asynchronous read on the ICMP -+socket failed in an unexpected fashion. The details of the error are provided -+as an argument of the log message. -+ -+% PING_CHECK_UNEXPECTED_WRITE_ERROR could not start next socket write %1 -+This error message occurs when initiating an asynchronous write on the ICMP -+socket failed in an unexpected fashion. The details of the error are provided -+as an argument of the log message. -+ -+% PING_CHECK_UNLOAD Ping Check hooks library has been unloaded -+This info message indicates that the Ping Check hooks library has been -+unloaded. -diff --git a/src/hooks/dhcp/ping_check/ping_check_mgr.cc b/src/hooks/dhcp/ping_check/ping_check_mgr.cc -new file mode 100644 -index 0000000000..cb4f2ee1dc ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/ping_check_mgr.cc -@@ -0,0 +1,798 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#include <config.h> -+ -+#include <ping_check_mgr.h> -+#include <ping_check_log.h> -+#include <dhcpsrv/cfgmgr.h> -+#include <hooks/hooks_manager.h> -+#include <util/multi_threading_mgr.h> -+#include <util/chrono_time_utils.h> -+ -+using namespace isc; -+using namespace isc::asiolink; -+using namespace isc::dhcp; -+using namespace isc::data; -+using namespace isc::hooks; -+using namespace isc::util; -+using namespace std; -+using namespace std::chrono; -+ -+namespace ph = std::placeholders; -+ -+namespace isc { -+namespace ping_check { -+ -+PingCheckMgr::PingCheckMgr() -+ : io_service_(new IOService()), thread_pool_(), -+ store_(new PingContextStore()), -+ channel_(), -+ config_cache_(new ConfigCache()), -+ mutex_(new mutex()), -+ suspended_(false) { -+} -+ -+PingCheckMgr::PingCheckMgr(uint32_t num_threads, -+ uint32_t min_echos, -+ uint32_t reply_timeout) -+ : io_service_(new IOService()), thread_pool_(), -+ store_(new PingContextStore()), -+ channel_(), -+ config_cache_(new ConfigCache()), -+ mutex_(new mutex()), -+ suspended_(false) { -+ PingCheckConfigPtr config(new PingCheckConfig()); -+ config->setMinPingRequests(min_echos); -+ config->setReplyTimeout(reply_timeout); -+ config->setPingChannelThreads(num_threads); -+ config_cache_->setGlobalConfig(config); -+} -+ -+PingCheckMgr::~PingCheckMgr() { -+ stop(); -+} -+ -+void -+PingCheckMgr::configure(ConstElementPtr params) { -+ if (!params) { -+ isc_throw(dhcp::DhcpConfigError, "params must not be null"); -+ return; -+ } -+ -+ if (params->getType() != Element::map) { -+ isc_throw(dhcp::DhcpConfigError, "params must be an Element::map"); -+ return; -+ } -+ -+ PingCheckConfigPtr config(new PingCheckConfig()); -+ config->parse(params); -+ config_cache_->setGlobalConfig(config); -+} -+ -+void -+PingCheckMgr::updateSubnetConfig(SrvConfigPtr server_config) { -+ // Iterate over subnets and cache configurations for each. -+ ConfigCachePtr local_cache(new ConfigCache()); -+ local_cache->setGlobalConfig(config_cache_->getGlobalConfig()); -+ auto const& subnets = server_config->getCfgSubnets4()->getAll(); -+ for (auto const& subnet : (*subnets)) { -+ auto user_context = subnet->getContext(); -+ local_cache->parseAndCacheConfig(subnet->getID(), user_context); -+ } -+ -+ // No errors above, replace the existing cache. -+ config_cache_ = local_cache; -+} -+ -+const PingCheckConfigPtr -+PingCheckMgr::getGlobalConfig() const { -+ return (config_cache_->getGlobalConfig()); -+} -+ -+const PingCheckConfigPtr -+PingCheckMgr::getScopedConfig(Lease4Ptr& lease) { -+ if (!lease) { -+ // This really shouldn't happen. -+ isc_throw(InvalidOperation, "PingCheckConfig::getScopedConfig() - lease cannot be empty"); -+ } -+ -+ auto subnet_id = lease->subnet_id_; -+ -+ // If the cache is stale, update it. We do this to catch subnets that have been updated -+ // via subnet_cmds. -+ auto server_config = CfgMgr::instance().getCurrentCfg(); -+ auto const& subnet = server_config->getCfgSubnets4()->getBySubnetId(subnet_id); -+ if (!subnet) { -+ // This really shouldn't happen. -+ isc_throw(InvalidOperation, "PingCheckMgr::getScopedConfig() - " -+ "no subnet for id: " << subnet_id -+ << ", for lease address: " << lease->addr_); -+ } -+ -+ // If cache is stale flush it and we'll lazy init subnets as we see them. -+ if (subnet->getModificationTime() > config_cache_->getLastFlushTime()) { -+ config_cache_->flush(); -+ } -+ -+ // If we don't find an entry for this subnet then we haven't seen it -+ // before so parse and cache it. If the subnet doesn't specify ping-check -+ // we cache an empty entry. -+ PingCheckConfigPtr config; -+ if (!config_cache_->findConfig(subnet_id, config)) { -+ auto user_context = subnet->getContext(); -+ try { -+ config = config_cache_->parseAndCacheConfig(subnet_id, user_context); -+ } catch (const std::exception& ex) { -+ // We emit and error and then cache an empty entry. This causes us -+ // to log the error once and then default to global settings afterward. -+ // This avoids us relentlessly logging and failing. Remember this -+ // is happening because a subnet was updated with an invalid context via -+ // subnet-cmd. -+ LOG_ERROR(ping_check_logger, PING_CHECK_MGR_SUBNET_CONFIG_FAILED) -+ .arg(subnet_id) -+ .arg(ex.what()); -+ config_cache_->cacheConfig(subnet_id, config); -+ } -+ } -+ -+ // Return subnet's ping-check config if it specified one, otherwise -+ // return the global config. -+ return (config ? config : config_cache_->getGlobalConfig()); -+} -+ -+void -+PingCheckMgr::startPing(dhcp::Lease4Ptr& lease, dhcp::Pkt4Ptr& query, hooks::ParkingLotHandlePtr& parking_lot, -+ const PingCheckConfigPtr& config) { -+ if (checkSuspended()) { -+ // Server should not be submitting requests. -+ isc_throw(InvalidOperation, "PingCheckMgr::startPing() - DHCP service is suspended!"); -+ } -+ -+ if (!channel_ || !channel_->isOpen()) { -+ isc_throw(InvalidOperation, "PingCheckMgr::startPing() - channel isn't open"); -+ } -+ -+ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC, -+ PING_CHECK_MGR_START_PING_CHECK) -+ .arg(lease->addr_) -+ .arg(query->getLabel()); -+ -+ // Adds a context to the store -+ store_->addContext(lease, query, config->getMinPingRequests(), -+ config->getReplyTimeout(), parking_lot); -+ -+ // Posts a call to channel's startSend() and startRead(). This will kick-start perpetual -+ // write and read cycles if they are not already running. -+ if (channel_) { -+ channel_->startSend(); -+ channel_->startRead(); -+ } -+} -+ -+void -+PingCheckMgr::startPing(dhcp::Lease4Ptr& lease, dhcp::Pkt4Ptr& query, hooks::ParkingLotHandlePtr& parking_lot) { -+ startPing(lease, query, parking_lot, getGlobalConfig()); -+} -+ -+bool -+PingCheckMgr::nextToSend(IOAddress& next) { -+ if (checkSuspended()) { -+ return (false); -+ } -+ -+ PingContextPtr context = store_->getNextToSend(); -+ if (!context) { -+ return (false); -+ } -+ -+ next = context->getTarget(); -+ // Transition to sending. -+ context->setState(PingContext::SENDING); -+ store_->updateContext(context); -+ -+ return (true); -+} -+ -+void -+PingCheckMgr::sendCompleted(const ICMPMsgPtr& echo, bool send_failed) { -+ if (checkSuspended()) { -+ return; -+ } -+ -+ try { -+ if (!echo) { -+ isc_throw(BadValue, "PingCheckMgr::sendCompleted() - echo is empty"); -+ } -+ -+ if (echo->getType() != ICMPMsg::ECHO_REQUEST) { -+ isc_throw(BadValue, "PingCheckMgr::sendCompleted() - message type: " -+ << echo->getType() << " is not an ECHO_REQUEST"); -+ } -+ -+ // Update the context associated with this ECHO_REQUEST. -+ PingContextPtr context = store_->getContextByAddress(echo->getDestination()); -+ if (!context) { -+ isc_throw(Unexpected, "PingCheckMgr::sendCompleted() " -+ " no context found for: " << echo->getDestination()); -+ } -+ -+ if (send_failed) { -+ // Recoverable error occurred which means we can't get to the target's -+ // network (interface down?). Treat this the same as TARGET UNREACHABLE. -+ finishFree(context); -+ } else { -+ // Transition the context to WAITING_FOR_REPLY. -+ context->beginWaitingForReply(); -+ store_->updateContext(context); -+ } -+ -+ // Update the expiration timer if necessary. -+ setNextExpiration(); -+ } catch (const std::exception& ex) { -+ LOG_ERROR(ping_check_logger, PING_CHECK_MGR_SEND_COMPLETED_ERROR) -+ .arg(ex.what()); -+ } -+} -+ -+void -+PingCheckMgr::replyReceived(const ICMPMsgPtr& reply) { -+ if (checkSuspended()) { -+ return; -+ } -+ -+ try { -+ if (!reply) { -+ isc_throw(BadValue, "PingCheckMgr::replyReceived() - echo is empty"); -+ } -+ -+ switch (reply->getType()) { -+ case ICMPMsg::ECHO_REPLY: -+ handleEchoReply(reply); -+ break; -+ case ICMPMsg::TARGET_UNREACHABLE: -+ // Extract embedded ECHO REQUEST -+ handleTargetUnreachable(reply); -+ break; -+ default: -+ // Ignore anything else. -+ return; -+ } -+ -+ setNextExpiration(); -+ } catch (const std::exception& ex) { -+ LOG_ERROR(ping_check_logger, PING_CHECK_MGR_REPLY_RECEIVED_ERROR) -+ .arg(ex.what()); -+ } -+} -+ -+void -+PingCheckMgr::handleEchoReply(const ICMPMsgPtr& echo_reply) { -+ // Update the context associated with this ECHO_REQUEST. -+ PingContextPtr context = store_->getContextByAddress(echo_reply->getSource()); -+ if (!context) { -+ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_DETAIL, -+ PING_CHECK_MGR_RECEIVED_UNEXPECTED_ECHO_REPLY) -+ .arg(echo_reply->getSource()) -+ .arg(echo_reply->getId()) -+ .arg(echo_reply->getSequence()); -+ return; -+ } -+ -+ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC, -+ PING_CHECK_MGR_RECEIVED_ECHO_REPLY) -+ .arg(echo_reply->getSource()) -+ .arg(echo_reply->getId()) -+ .arg(echo_reply->getSequence()); -+ -+ context->setState(PingContext::TARGET_IN_USE); -+ store_->updateContext(context); -+ -+ // If parking is employed, unpark the query from the parking lot, -+ // and set the offer_address_in_use argument in the callout handle -+ // to true, indicating to the server that the lease should be declined -+ // and the DHCPOFFER discarded. -+ auto parking_lot = context->getParkingLot(); -+ if (parking_lot) { -+ auto query = context->getQuery(); -+ auto callout_handle = query->getCalloutHandle(); -+ callout_handle->setArgument("offer_address_in_use", true); -+ parking_lot->unpark(query); -+ } -+ -+ // Remove the context from the store. -+ store_->deleteContext(context); -+} -+ -+void -+PingCheckMgr::handleTargetUnreachable(const ICMPMsgPtr& unreachable) { -+ // Unpack the embedded ECHO REQUEST. -+ ICMPMsgPtr embedded_echo; -+ auto payload = unreachable->getPayload(); -+ embedded_echo = ICMPMsg::unpack(payload.data(), payload.size()); -+ -+ // Fetch the context associated with the ECHO_REQUEST. -+ PingContextPtr context = store_->getContextByAddress(embedded_echo->getDestination()); -+ if (!context) { -+ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_DETAIL, -+ PING_CHECK_MGR_RECEIVED_UNEXPECTED_UNREACHABLE_MSG) -+ .arg(embedded_echo->getDestination()) -+ .arg(embedded_echo->getId()) -+ .arg(embedded_echo->getSequence()); -+ return; -+ } -+ -+ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_DETAIL, -+ PING_CHECK_MGR_RECEIVED_UNREACHABLE_MSG) -+ .arg(embedded_echo->getDestination()) -+ .arg(embedded_echo->getId()) -+ .arg(embedded_echo->getSequence()); -+ -+ // Render the address usable. -+ finishFree(context); -+} -+ -+void -+PingCheckMgr::finishFree(const PingContextPtr& context) { -+ context->setState(PingContext::TARGET_FREE); -+ store_->updateContext(context); -+ -+ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC, -+ PING_CHECK_MGR_LEASE_FREE_TO_USE) -+ .arg(context->getTarget()) -+ .arg(context->getQuery()->getLabel()); -+ -+ // If parking is employed, unpark the query from the parking lot, -+ // and set the offer_address_in_use argument in the callout handle -+ // to false, indicating to the server that the lease is available -+ // and the DHCPOFFER should be sent to the client. -+ auto parking_lot = context->getParkingLot(); -+ if (parking_lot) { -+ auto query = context->getQuery(); -+ auto callout_handle = query->getCalloutHandle(); -+ callout_handle->setArgument("offer_address_in_use", false); -+ parking_lot->unpark(context->getQuery()); -+ } -+ -+ // Remove the context from the store. -+ store_->deleteContext(context); -+} -+ -+void -+PingCheckMgr::channelShutdown() { -+ LOG_ERROR(ping_check_logger, PING_CHECK_MGR_CHANNEL_DOWN); -+ if (io_service_) { -+ // As this is a callback that may be invoked by a channel -+ // thread we post a call to stopService() rather than call -+ // it directly. -+ io_service_->post([&]() { stopService(true); }); -+ } -+} -+ -+size_t -+PingCheckMgr::processExpiredSince(const TimeStamp& since /* = PingContext::now() */) { -+ auto expired_pings = store_->getExpiredSince(since); -+ size_t more_pings = 0; -+ for (auto const& context : *(expired_pings)) { -+ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_DETAIL, -+ PING_CHECK_MGR_REPLY_TIMEOUT_EXPIRED) -+ .arg(context->getTarget()) -+ .arg(context->getEchosSent()) -+ .arg(context->getMinEchos()) -+ .arg(context->getReplyTimeout()); -+ -+ if (context->getEchosSent() < context->getMinEchos()) { -+ doNextEcho(context); -+ ++more_pings; -+ } else { -+ finishFree(context); -+ } -+ } -+ -+ return (more_pings); -+} -+ -+void -+PingCheckMgr::doNextEcho(const PingContextPtr& context) { -+ // Position to do another ping by re-entering WAITING_TO_SEND -+ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_DETAIL, -+ PING_CHECK_MGR_NEXT_ECHO_SCHEDULED) -+ .arg(context->getTarget()) -+ .arg(context->getEchosSent() + 1) -+ .arg(context->getMinEchos()); -+ -+ context->beginWaitingToSend(); -+ store_->updateContext(context); -+} -+ -+TimeStamp -+PingCheckMgr::getNextExpiry() { -+ MultiThreadingLock lock(*mutex_); -+ return (next_expiry_); -+} -+ -+void -+PingCheckMgr::setNextExpiration() { -+ MultiThreadingLock lock(*mutex_); -+ if (checkSuspendedInternal()) { -+ return; -+ } -+ -+ setNextExpirationInternal(); -+} -+ -+void -+PingCheckMgr::setNextExpirationInternal() { -+ // Find the context that expires soonest. -+ PingContextPtr context = store_->getExpiresNext(); -+ if (context) { -+ // if the context's expiry is sooner than current expiry -+ // reschedule expiration timer -+ if ((next_expiry_ == PingContext::EMPTY_TIME()) || -+ (context->getNextExpiry() < next_expiry_)) { -+ auto now = PingContext::now(); -+ auto timeout = duration_cast<milliseconds>(context->getNextExpiry() - now); -+ /// @todo For now we'll impose a 2 ms minimum to avoid thrashing the timer. -+ timeout = (timeout > milliseconds(2) ? timeout : milliseconds(2)); -+ next_expiry_ = now + timeout; -+ expiration_timer_->setup(std::bind(&PingCheckMgr::expirationTimedOut, -+ shared_from_this()), -+ timeout.count(), IntervalTimer::ONE_SHOT); -+ } -+ } else { -+ // Nothing waiting to expire. Cancel the timer. -+ cancelExpirationTimerInternal(); -+ } -+} -+ -+void -+PingCheckMgr::cancelExpirationTimer() { -+ MultiThreadingLock lock(*mutex_); -+ cancelExpirationTimerInternal(); -+} -+ -+void -+PingCheckMgr::cancelExpirationTimerInternal() { -+ if (expiration_timer_) { -+ expiration_timer_->cancel(); -+ next_expiry_ = PingContext::EMPTY_TIME(); -+ } -+} -+ -+void -+PingCheckMgr::expirationTimedOut() { -+ MultiThreadingLock lock(*mutex_); -+ if (checkSuspendedInternal()) { -+ return; -+ } -+ -+ // Process everything that has expired since current time. -+ auto more_pings = processExpiredSince(); -+ -+ // Update the expiration timer. -+ next_expiry_ = PingContext::EMPTY_TIME(); -+ setNextExpirationInternal(); -+ -+ // In the event there was nothing left to process when timed out, -+ // poke the channel to make sure things are moving. -+ if (more_pings && channel_) { -+ channel_->startSend(); -+ channel_->startRead(); -+ } -+} -+ -+CalloutHandle::CalloutNextStep -+PingCheckMgr::shouldPing(Lease4Ptr& lease, Pkt4Ptr& query, -+ Lease4Ptr& old_lease, -+ const PingCheckConfigPtr& config) { -+ -+ // If ping-check is disabled or the channel isn't open, -+ // drop the query from parking and release the offer to the client. -+ if (!config->getEnablePingCheck() || !channel_ || !channel_->isOpen()) { -+ return (CalloutHandle::CalloutNextStep::NEXT_STEP_CONTINUE); -+ } -+ -+ // If we're already running check on this address then drop the -+ // query from parking and discard the offer. -+ if (store_->getContextByAddress(lease->addr_)) { -+ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC, -+ PING_CHECK_DUPLICATE_CHECK) -+ .arg(lease->addr_) -+ .arg(query->getLabel()); -+ return (CalloutHandle::CalloutNextStep::NEXT_STEP_DROP); -+ } -+ -+ // If there's a previous lease that belongs to this client and -+ // it was touched by the client less than ping-cltt-secs ago then -+ // no check is needed. Drop the query from parking and release the -+ // offer to the client, -+ if (old_lease && (old_lease->addr_ == lease->addr_)) { -+ if (old_lease->belongsToClient(lease->hwaddr_, lease->client_id_)) { -+ auto now = time(0); -+ if ((now - old_lease->cltt_) < config->getPingClttSecs()) { -+ return (CalloutHandle::CalloutNextStep::NEXT_STEP_CONTINUE); -+ } -+ } -+ } -+ -+ // Leave it parked and do the ping check. -+ return (CalloutHandle::CalloutNextStep::NEXT_STEP_PARK); -+} -+ -+void -+PingCheckMgr::startService(NetworkStatePtr network_state) { -+ network_state_ = network_state; -+ io_service_->post([&]() { start(); }); -+} -+ -+bool -+PingCheckMgr::checkSuspended() { -+ MultiThreadingLock lock(*mutex_); -+ return (checkSuspendedInternal()); -+} -+ -+bool -+PingCheckMgr::checkSuspendedInternal() { -+ if (!network_state_ || network_state_->isServiceEnabled()) { -+ suspended_ = false; -+ } else { -+ if (!suspended_) { -+ suspended_ = true; -+ -+ // Flush the context store, dropping parked queries. -+ flush(false); -+ } -+ } -+ -+ return (suspended_); -+} -+ -+void -+PingCheckMgr::stopService(bool finish_free) { -+ // Pause the thread pool while we flush the store. -+ pause(); -+ -+ // Flush the context store. If finish_free is true -+ // the flush will treat the remaining context lease -+ // addresses as free to use and unpark them. This -+ // will cause the server to send out the associated -+ // OFFERs. If it's false we just drop them from -+ // the parking lot. -+ flush(finish_free); -+ -+ // Stop the thread pool, destroy the channel and the like. -+ stop(); -+} -+ -+void -+PingCheckMgr::start() { -+ if (MultiThreadingMgr::instance().isTestMode()) { -+ return; -+ } -+ if (!MultiThreadingMgr::instance().getMode()) { -+ startSingleThreaded(); -+ return; -+ } -+ -+ // We must be in multi-threading mode. -+ // Add critical section callbacks. -+ MultiThreadingMgr::instance().addCriticalSectionCallbacks("PING_CHECK", -+ std::bind(&PingCheckMgr::checkPermissions, this), -+ std::bind(&PingCheckMgr::pause, this), -+ std::bind(&PingCheckMgr::resume, this)); -+ -+ // Punt if we're already started. -+ if (thread_pool_ && thread_pool_->isStopped()) { -+ isc_throw(InvalidOperation, "PingCheckMgr already started!"); -+ } -+ -+ try { -+ auto config = config_cache_->getGlobalConfig(); -+ auto use_threads = (config->getPingChannelThreads() ? config->getPingChannelThreads() -+ : MultiThreadingMgr::instance().getThreadPoolSize()); -+ thread_pool_.reset(new IoServiceThreadPool(IOServicePtr(), use_threads, true)); -+ IOServicePtr pool_ios = thread_pool_->getIOService(); -+ channel_ = createChannel(pool_ios); -+ channel_->open(); -+ expiration_timer_.reset(new IntervalTimer(pool_ios)); -+ thread_pool_->run(); -+ LOG_INFO(ping_check_logger, PING_CHECK_MGR_STARTED) -+ .arg(use_threads); -+ } catch (const std::exception& ex) { -+ channel_.reset(); -+ thread_pool_.reset(); -+ isc_throw(Unexpected, "PingCheckMgr::start failed:" << ex.what()); -+ } -+} -+ -+void -+PingCheckMgr::startSingleThreaded() { -+ try { -+ auto config = config_cache_->getGlobalConfig(); -+ channel_ = createChannel(io_service_); -+ channel_->open(); -+ expiration_timer_.reset(new IntervalTimer(io_service_)); -+ LOG_INFO(ping_check_logger, PING_CHECK_MGR_STARTED_SINGLE_THREADED); -+ } catch (const std::exception& ex) { -+ channel_.reset(); -+ isc_throw(Unexpected, "PingCheckMgr::startSingleThreaded() failed:" << ex.what()); -+ } -+} -+ -+PingChannelPtr -+PingCheckMgr::createChannel(IOServicePtr io_service) { -+ return (PingChannelPtr(new PingChannel(io_service, -+ std::bind(&PingCheckMgr::nextToSend, -+ this, ph::_1), -+ std::bind(&PingCheckMgr::sendCompleted, -+ this, ph::_1, ph::_2), -+ std::bind(&PingCheckMgr::replyReceived, -+ this, ph::_1), -+ std::bind(&PingCheckMgr::channelShutdown, -+ this)))); -+} -+ -+void -+PingCheckMgr::checkPermissions() { -+ // Since this function is used as CS callback all exceptions must be -+ // suppressed, unlikely though they may be. -+ try { -+ if (thread_pool_) { -+ thread_pool_->checkPausePermissions(); -+ } -+ } catch (const isc::MultiThreadingInvalidOperation& ex) { -+ LOG_ERROR(ping_check_logger, PING_CHECK_PAUSE_ILLEGAL) -+ .arg(ex.what()); -+ // The exception needs to be propagated to the caller of the -+ // @ref MultiThreadingCriticalSection constructor. -+ throw; -+ } catch (const std::exception& ex) { -+ LOG_ERROR(ping_check_logger, PING_CHECK_PAUSE_PERMISSIONS_FAILED) -+ .arg(ex.what()); -+ } -+} -+ -+void -+PingCheckMgr::pause() { -+ if (!MultiThreadingMgr::instance().getMode()) { -+ return; -+ } -+ -+ // Since this function is used as CS callback all exceptions must be -+ // suppressed, unlikely though they may be. -+ try { -+ // Cancel the expiration timer. -+ cancelExpirationTimer(); -+ -+ // Pause the thread pool. -+ if (thread_pool_) { -+ thread_pool_->pause(); -+ } -+ } catch (const std::exception& ex) { -+ LOG_ERROR(ping_check_logger, PING_CHECK_PAUSE_FAILED) -+ .arg(ex.what()); -+ } -+} -+ -+void -+PingCheckMgr::resume() { -+ if (!MultiThreadingMgr::instance().getMode()) { -+ return; -+ } -+ -+ // Since this function is used as CS callback all exceptions must be -+ // suppressed, unlikely though they may be. -+ try { -+ if (thread_pool_) { -+ thread_pool_->run(); -+ } -+ -+ // Restore the expiration timer. -+ setNextExpiration(); -+ } catch (const std::exception& ex) { -+ LOG_ERROR(ping_check_logger, PING_CHECK_RESUME_FAILED) -+ .arg(ex.what()); -+ } -+} -+ -+void -+PingCheckMgr::stop() { -+ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC, PING_CHECK_MGR_STOPPING); -+ -+ // Cancel the expiration timer. -+ cancelExpirationTimer(); -+ -+ if (channel_) { -+ channel_->close(); -+ } -+ -+ if (thread_pool_) { -+ // Remove critical section callbacks. -+ MultiThreadingMgr::instance().removeCriticalSectionCallbacks("PING_CHECK"); -+ -+ // Stop the thread pool. -+ thread_pool_->stop(); -+ -+ thread_pool_->getIOService()->stopAndPoll(); -+ -+ // Ditch the thread_pool -+ thread_pool_.reset(); -+ } -+ // Ditch the timer. It must be destroyed before the thread pool because in -+ // MT it holds a reference to the pool's IOService. -+ expiration_timer_.reset(); -+ -+ // Get rid of the channel. -+ channel_.reset(); -+ -+ if (io_service_) { -+ io_service_->stopAndPoll(); -+ } -+ -+ LOG_INFO(ping_check_logger, PING_CHECK_MGR_STOPPED); -+} -+ -+bool -+PingCheckMgr::isRunning() { -+ // In ST mode, running is an open channel. -+ if (!MultiThreadingMgr::instance().getMode()) { -+ return (channel_ && channel_->isOpen()); -+ } -+ -+ if (thread_pool_) { -+ return (thread_pool_->isRunning()); -+ } -+ -+ return (false); -+} -+ -+bool -+PingCheckMgr::isStopped() { -+ // In ST mode, stopped equates to no channel. -+ if (!MultiThreadingMgr::instance().getMode()) { -+ return (!channel_); -+ } -+ -+ if (thread_pool_) { -+ return (thread_pool_->isStopped()); -+ } -+ -+ return (true); -+} -+ -+bool -+PingCheckMgr::isPaused() { -+ if (thread_pool_) { -+ return (thread_pool_->isPaused()); -+ } -+ -+ return (false); -+} -+ -+void -+PingCheckMgr::flush(bool finish_free /* = false */) { -+ if (!store_) { -+ return; -+ } -+ -+ // Fetch them all. -+ auto contexts = store_->getAll(); -+ for (auto const& context : *contexts) { -+ if (finish_free) { -+ finishFree(context); -+ } else { -+ auto parking_lot = context->getParkingLot(); -+ if (parking_lot) { -+ parking_lot->drop(context->getQuery()); -+ } -+ } -+ } -+ -+ store_->clear(); -+} -+ -+} // end of namespace ping_check -+} // end of namespace isc -diff --git a/src/hooks/dhcp/ping_check/ping_check_mgr.h b/src/hooks/dhcp/ping_check/ping_check_mgr.h -new file mode 100644 -index 0000000000..42d11c1b48 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/ping_check_mgr.h -@@ -0,0 +1,436 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#ifndef PING_CHECK_MGR_H -+#define PING_CHECK_MGR_H -+ -+#include <asiolink/interval_timer.h> -+#include <asiolink/io_address.h> -+#include <asiolink/io_service.h> -+#include <asiolink/io_service_thread_pool.h> -+#include <cc/data.h> -+#include <cc/simple_parser.h> -+#include <dhcpsrv/srv_config.h> -+#include <hooks/callout_handle.h> -+#include <dhcp/pkt4.h> -+#include <dhcpsrv/lease.h> -+#include <dhcpsrv/network_state.h> -+#include <ping_context_store.h> -+#include <ping_channel.h> -+#include <config_cache.h> -+ -+#include <boost/enable_shared_from_this.hpp> -+ -+#include <mutex> -+ -+namespace isc { -+namespace ping_check { -+ -+/// @brief Defines a pointer to a PingContextStore. -+typedef boost::shared_ptr<PingContextStore> PingContextStorePtr; -+ -+/// @brief Ping Check Manager. -+/// -+/// PinCheckMgr carries out the higher order management of requests for ping -+/// checks from the server. It is a singleton, instantiated when the library -+/// is loaded. It is responsible for: -+/// 1. Parsing and applying configuration. -+/// 2. Maintaining in-memory store of current ping requests (PingContextStore). -+/// 3. Creating and managing the PingChannel through which individual ICMP ECHO/REPLY -+/// cycles are conducted. -+/// 4. When in multi-threaded mode, it creates an IOServiceThread and synchronizes -+/// its state with Kea core MT. -+class PingCheckMgr : public boost::enable_shared_from_this<PingCheckMgr> { -+public: -+ /// @brief Constructor. -+ explicit PingCheckMgr(); -+ -+ /// @brief Constructor. -+ /// -+ /// This constructor is used in testing. It permits setting some basic behavior -+ /// parameters directly, rather than requiring calls to @c configure(). -+ /// -+ /// @param num_threads number of threads to use in the thread pool (0 means follow -+ /// core thread pool size). -+ /// @param min_echos minimum number of ECHO REQUESTs sent without replies -+ /// received required to declare an address free to offer. Defaults to 1, -+ /// must be greater than zero. -+ /// @param reply_timeout maximum number of milliseconds to wait for an -+ /// ECHO REPLY after an ECHO REQUEST has been sent. Defaults to 100. -+ PingCheckMgr(uint32_t num_threads, -+ uint32_t min_echos = 1, -+ uint32_t reply_timeout = 100); -+ -+ /// @brief Destructor. -+ virtual ~PingCheckMgr(); -+ -+ /// @brief Configure the PingCheckMgr. -+ /// -+ /// @param params map containing the hook library parameters. -+ /// @throw BadValue and similar exceptions on error. -+ void configure(data::ConstElementPtr params); -+ -+ /// @brief Update the cache of subnet ping check configurations. -+ /// -+ /// Iterates over the subnets in the given server configuration, -+ /// and caches their ping-check configuration. -+ /// -+ /// @param server_config Server configuration containing the -+ /// configured subnets to process. -+ void updateSubnetConfig(dhcp::SrvConfigPtr server_config); -+ -+ /// @brief Creates a ping channel instance. -+ /// -+ /// @param io_service IOService that will drive the channel. -+ /// -+ /// @return pointer to the newly created channel. -+ virtual PingChannelPtr createChannel(asiolink::IOServicePtr io_service); -+ -+ /// @brief Initiates a ping check for a given lease and its associated -+ /// DHCPDISCOVER packet. -+ /// -+ /// Adds a context to the store and posts a call to @c PingChannel::startSend(). -+ /// -+ /// @param lease lease whose address needs to be ping checked. -+ /// @param query parked DHCPDISCOVER associated with the lease. -+ /// @param parking_lot parking lot in which query is parked. If empty, -+ /// parking is assumed to not be employed. -+ /// @param config configuration parameters to employ. -+ void startPing(dhcp::Lease4Ptr& lease, dhcp::Pkt4Ptr& query, -+ hooks::ParkingLotHandlePtr& parking_lot, -+ const PingCheckConfigPtr& config); -+ -+ /// @brief Initiates a ping check for a given lease and its associated -+ /// DHCPDISCOVER packet. -+ /// -+ /// Convenience method used in unit tests which uses global -+ /// configuration parameters only. -+ /// -+ /// @param lease lease whose address needs to be ping checked. -+ /// @param query parked DHCPDISCOVER associated with the lease. -+ /// @param parking_lot parking lot in which query is parked. If empty, -+ /// parking is assumed to not be employed. -+ void startPing(dhcp::Lease4Ptr& lease, dhcp::Pkt4Ptr& query, -+ hooks::ParkingLotHandlePtr& parking_lot); -+ -+ /// @brief Callback passed to PingChannel to use to retrieve the next -+ /// address to check. -+ /// -+ /// Fetches the context which has been in the WAITING_TO_SEND state the -+ /// longest and returns its lease address. -+ /// -+ /// @param[out] next upon return it will contain the next target address. -+ /// Contents are only meaningful if the function returns true. -+ /// -+ /// @return True another target address exists, false otherwise. -+ virtual bool nextToSend(asiolink::IOAddress& next); -+ -+ /// @brief Callback passed to PingChannel to invoke when an ECHO REQUEST -+ /// send has completed. -+ /// -+ /// If the send completed successfully we'll transition the context to -+ /// WAITING_FOR_REPLY, update the context in the store, and the update -+ /// next expiration. -+ /// -+ /// If the send failed, this implies that a recoverable error occurred, such -+ /// as a interface being down and thus, there is currently no way to send -+ /// the ping to the target network. We'll treat this the same as an ICMP -+ /// TARGET_UNREACHABLE and release the OFFER by calling @c finishFree(). -+ /// -+ /// @param echo ICMP echo message that as sent. -+ /// @param send_failed True if the send completed with a non-fatal error, -+ /// false otherwise. -+ virtual void sendCompleted(const ICMPMsgPtr& echo, bool send_failed); -+ -+ /// @brief Callback passed to PingChannel to invoke when an ICMP -+ /// reply has been received. -+ /// -+ /// If the reply type is an ECHO REQUEST, it is passed to -+ /// handleEchoRequest(), if it is an UNREACHABLE message it -+ /// is passed to handleTargetUnreachable(), any other message -+ /// type is dropped on the floor and the function returns. -+ /// Upon handler completion, it calls setNextExpiration() to -+ /// update the expiration timer. -+ /// -+ /// @param reply ICMP message that was received. -+ virtual void replyReceived(const ICMPMsgPtr& reply); -+ -+ /// @brief Process an ECHO REPLY message. -+ /// -+ /// @param echo_reply ICMP ECHO REPLY message to process. -+ void handleEchoReply(const ICMPMsgPtr& echo_reply); -+ -+ /// @brief Process an UNREACHABLE message. -+ /// -+ /// @param unreachable ICMP UNREACHABLE message to process. -+ void handleTargetUnreachable(const ICMPMsgPtr& unreachable); -+ -+ /// @brief Processes a context whose address has been deemed free to use. -+ /// -+ /// -# Moves the context to TARGET_FREE state -+ /// -# Updates the context in the store -+ /// -# Unparks the query which will release the DHCPOFFER to the client -+ /// -# Invokes the target free callback (do we still need this?) -+ /// -# Deletes the store from the context -+ /// -+ /// @param context context to process. -+ void finishFree(const PingContextPtr& context); -+ -+ /// @brief Position a context to do another ping test. -+ /// -+ /// -# Moves the context to WAITING_SEND_STATE -+ /// -# Updates the context in the store -+ /// -+ /// @param context context to process. -+ void doNextEcho(const PingContextPtr& context); -+ -+ /// @brief Callback passed to PingChannel to invoke when it shuts down. -+ /// -+ /// Logs the shutdown and then posts a call to @c stopService() to the -+ /// main IOService. -+ virtual void channelShutdown(); -+ -+ /// @brief Performs expiration processing for contexts whose WAITING_FOR_REPLY -+ /// states expired prior to a given point in time. -+ /// -+ /// expired_pings = store_->getExpiredSince(since) -+ /// for context : expired_pings { -+ /// unpark context->getQuery() -+ /// store_->deleteContext(context) -+ /// } -+ /// -+ /// @param since point in time to select against. Defaults to current time. -+ /// @return number of contexts scheduled for another ping, zero if none. -+ virtual size_t processExpiredSince(const TimeStamp& since = PingContext::now()); -+ -+ /// @brief Fetches the time at which expiration timer will next expire. -+ /// -+ /// @return TimeStamp containing the next expiration time. -+ TimeStamp getNextExpiry(); -+ -+ /// @brief Updates the expiration timer (thread safe). -+ /// -+ /// PingContextPtr next = pings->getExpiresNext() -+ /// if next -+ /// reschedule expiration timer for next->getNextExpiry(); -+ /// else -+ /// cancel expiration timer -+ virtual void setNextExpiration(); -+ -+ /// @brief Updates the expiration timer. -+ /// -+ /// PingContextPtr next = pings->getExpiresNext() -+ /// if next -+ /// reschedule expiration timer for next->getNextExpiry(); -+ /// else -+ /// cancel expiration timer -+ virtual void setNextExpirationInternal(); -+ -+ /// @brief Cancels the expiration timer (thread safe). -+ void cancelExpirationTimer(); -+ -+ /// @brief Cancels the expiration timer. -+ void cancelExpirationTimerInternal(); -+ -+ /// @brief Callback passed to expiration timer to invoke on timeout. -+ virtual void expirationTimedOut(); -+ -+ /// @brief Determines whether or not a lease should be ping checked. -+ /// -+ /// Employs the following logic to determine if a ping-check should -+ /// be conducted: -+ /// -+ /// If there's a previous lease that belongs to this client and -+ /// it was touched by the client less than ping-cltt-secs ago, -+ /// then send the offer to the client without ping checking. -+ /// -+ /// Otherwise a ping-check is called for, leave the query parked. -+ /// -+ /// @param lease prospective lease to check. -+ /// @param query DHCPDISCOVER associated with the lease. -+ /// @param old_lease pre-existing lease for this client (if one). -+ /// @param config configuration parameters to employ. -+ /// -+ /// @return CalloutNextStep indicating what should happen next: -+ /// - status == PARK - ping check it -+ /// - status == CONTINUE - check not needed, release DHCPOFFER to client -+ /// - status == DROP - duplicate check, drop the duplicate DHCPOFFER -+ virtual hooks::CalloutHandle::CalloutNextStep shouldPing(dhcp::Lease4Ptr& lease, -+ dhcp::Pkt4Ptr& query, -+ dhcp::Lease4Ptr& old_lease, -+ const PingCheckConfigPtr& config); -+ -+ /// @brief Check if the current thread can perform thread pool state -+ /// transition. -+ /// -+ /// @throw MultiThreadingInvalidOperation if the state transition is done on -+ /// any of the worker threads. -+ void checkPermissions(); -+ -+ /// @brief Performs a deferred start by posting an invocation of @c start() -+ /// to the given IOService. -+ /// -+ /// @param network_state pointer to server's networks state object. -+ void startService(dhcp::NetworkStatePtr network_state); -+ -+ /// @brief Shuts down the manager's channel, flushes the store. -+ /// -+ /// This function gracefully winds down operation: -+ /// -+ /// 1. Pauses the thread pool. -+ /// 2. Flushes the context store, either finishing all contexts as free -+ /// or just dropping them from parking, depending on finish_free parameter. -+ /// 3. Stop the thread pool, shutdown the channel. -+ /// -+ /// @param finish_free if true finishFree() will be invoke on all remaining -+ /// contexts in the store, otherwise their queries are simply dropped from -+ /// the parking lot. -+ void stopService(bool finish_free = false); -+ -+ /// @brief Start PingChannel operations. -+ /// -+ /// Will start multi-threaded if core MT is enabled, or calls -+ /// @c startSingleThreaded() if core MT is disabled. Creates -+ /// a thread pool with its own IOService, uses that IOService -+ /// when creating the channel. -+ void start(); -+ -+ /// @brief Start single-threaded PingChannel operations. -+ /// -+ /// Does not create a thread pool. Uses main thread's IOService -+ /// when creating the channel. -+ void startSingleThreaded(); -+ -+ /// @brief Pause PingChannel operations. -+ /// -+ /// In multi-threaded mode this pauses the thread pool threads, in -+ /// single-threaded mode it does nothing. -+ void pause(); -+ -+ /// @brief Resume PingChannel operations. -+ /// -+ /// In multi-threaded mode this resumes the thread pool threads, in -+ /// single-threaded mode it does nothing. -+ void resume(); -+ -+ /// @brief Flushes the ping context store. -+ /// -+ /// This function iterates over the contexts in the store and then -+ /// either invokes finishFree() or drops their queries from parking -+ /// depending upon finish_free parameter. It assumes the operations -+ /// have ceased (i.e. thread pool is not running). -+ /// -+ /// @param finish_free if true finishFree() will be invoke on all remaining -+ /// contexts in the store, otherwise their queries are simply dropped from -+ /// the parking lot. -+ void flush(bool finish_free = false); -+ -+ /// @brief Stop PingChannel operations. -+ void stop(); -+ -+ /// @brief Indicates if the thread pool is running. -+ /// -+ /// @return True if the thread pool exists and it is in the RUNNING state in -+ /// multi-threaded mode, true if the channel exists and is open in single-threaded -+ /// mode, false otherwise. -+ bool isRunning(); -+ -+ /// @brief Indicates if the thread pool is stopped. -+ /// -+ /// @return True if the thread pool does not exist or it is in the STOPPED -+ /// state in multi-threaded mode, true if the channel does not exist in -+ /// single-threaded mode, false otherwise. -+ bool isStopped(); -+ -+ /// @brief Indicates if the thread pool is paused. -+ /// -+ /// @return True if the thread pool exists and it is in the PAUSED state, -+ /// false otherwise. Always returns false in single-threaded mode. -+ bool isPaused(); -+ -+ /// @brief Checks if operations are currently suspended due to NetworkState. -+ /// -+ /// Thread-safe wrapper around checkSuspendedInternal(). -+ /// -+ /// @return True if operations are suspended, false otherwise. -+ bool checkSuspended(); -+ -+ /// @brief Checks if operations are currently suspended due to NetworkState. -+ /// -+ /// If DHCP service is enabled, operations are not suspended and the function -+ /// returns false. Otherwise operations, if not already suspended, are suspended -+ /// by flushing the PingContext store and the function returns true. The queries -+ /// for flushed contexts are dropped from parking and thus their offers discarded. -+ /// -+ /// @return True if operations are suspended, false otherwise. -+ bool checkSuspendedInternal(); -+ -+ /// @brief Fetches the current, global configuration parameters. -+ /// -+ /// @return PingCheckConfig reference containing the current configuration. -+ const PingCheckConfigPtr getGlobalConfig() const; -+ -+ /// @brief Fetches the current, scoped configuration parameters. -+ /// -+ /// @param lease lease for which the parameters are desired. -+ /// -+ /// @return PingCheckConfig reference containing the current configuration. -+ const PingCheckConfigPtr getScopedConfig(dhcp::Lease4Ptr& lease); -+ -+ /// @brief Get the hook I/O service. -+ /// -+ /// @return the hook I/O service. -+ isc::asiolink::IOServicePtr getIOService() { -+ return (io_service_); -+ } -+ -+ /// @brief Set the hook I/O service. -+ /// -+ /// @param io_service the hook I/O service. -+ void setIOService(isc::asiolink::IOServicePtr io_service) { -+ io_service_ = io_service; -+ } -+ -+protected: -+ -+ /// @brief The hook I/O service. -+ isc::asiolink::IOServicePtr io_service_; -+ -+ /// @brief Thread pool used when running multi-threaded. -+ asiolink::IoServiceThreadPoolPtr thread_pool_; -+ -+ /// @brief In-memory store of PingContexts. -+ PingContextStorePtr store_; -+ -+ /// @brief Channel that conducts ICMP messaging. -+ PingChannelPtr channel_; -+ -+ /// @brief Warehouses parsed global and subnet configuration. -+ ConfigCachePtr config_cache_; -+ -+ /// @brief Tracks whether or not the server is processing DHCP packets. -+ dhcp::NetworkStatePtr network_state_; -+ -+ /// @brief TimeStamp of the next expiration event. -+ TimeStamp next_expiry_; -+ -+ /// @brief Timer which tracks the next expiration event. -+ asiolink::IntervalTimerPtr expiration_timer_; -+ -+ /// @brief The mutex used to protect internal state. -+ const boost::scoped_ptr<std::mutex> mutex_; -+ -+ /// @brief Indicates whether or not operations have been suspended. -+ bool suspended_; -+}; -+ -+/// @brief Defines a shared pointer to a PingCheckMgr. -+typedef boost::shared_ptr<PingCheckMgr> PingCheckMgrPtr; -+ -+} // end of namespace ping_check -+} // end of namespace isc -+ -+#endif -diff --git a/src/hooks/dhcp/ping_check/ping_context.cc b/src/hooks/dhcp/ping_check/ping_context.cc -new file mode 100644 -index 0000000000..45e896f948 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/ping_context.cc -@@ -0,0 +1,237 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#include <config.h> -+ -+#include <ping_context.h> -+#include <ping_check_log.h> -+#include <exceptions/exceptions.h> -+#include <util/chrono_time_utils.h> -+#include <iostream> -+ -+using namespace std; -+using namespace isc; -+using namespace isc::asiolink; -+using namespace isc::dhcp; -+using namespace isc::hooks; -+using namespace std::chrono; -+ -+namespace isc { -+namespace ping_check { -+ -+PingContext::PingContext(Lease4Ptr& lease, Pkt4Ptr& query, -+ uint32_t min_echos /* = 1 */, -+ uint32_t reply_timeout /* = 100 */, -+ ParkingLotHandlePtr& parking_lot /* = EMPTY_LOT() */) -+ : min_echos_(min_echos), -+ reply_timeout_(reply_timeout), -+ echos_sent_(0), -+ last_echo_sent_time_(EMPTY_TIME()), -+ send_wait_start_(EMPTY_TIME()), -+ next_expiry_(EMPTY_TIME()), -+ created_time_(PingContext::now()), -+ lease_(lease), -+ query_(query), -+ state_(NEW), -+ parking_lot_(parking_lot) { -+ if (!lease_) { -+ isc_throw(BadValue, "PingContext ctor - lease cannot be empty"); -+ } -+ -+ if (!query_) { -+ isc_throw(BadValue, "PingContext ctor - query cannot be empty"); -+ } -+ -+ if (getTarget() == IOAddress::IPV4_ZERO_ADDRESS()) { -+ isc_throw(BadValue, "PingContext ctor - target address cannot be 0.0.0.0"); -+ } -+ -+ if (min_echos_ == 0) { -+ isc_throw(BadValue, "PingContext ctor - min_echos must be greater than 0"); -+ } -+ -+ if (reply_timeout_ == 0) { -+ isc_throw(BadValue, "PingContext ctor - reply_timeout must be greater than 0"); -+ } -+} -+ -+PingContext::State -+PingContext::stringToState(const std::string& state_str) { -+ if (state_str == "NEW") { -+ return (NEW); -+ } -+ -+ if (state_str == "WAITING_TO_SEND") { -+ return (WAITING_TO_SEND); -+ } -+ -+ if (state_str == "SENDING") { -+ return (SENDING); -+ } -+ -+ if (state_str == "WAITING_FOR_REPLY") { -+ return (WAITING_FOR_REPLY); -+ } -+ -+ if (state_str == "TARGET_FREE") { -+ return (TARGET_FREE); -+ } -+ -+ if (state_str == "TARGET_IN_USE") { -+ return (TARGET_IN_USE); -+ } -+ -+ isc_throw(BadValue, "Invalid PingContext::State: '" << state_str << "'"); -+} -+ -+TimeStamp -+PingContext::now() { -+ return (time_point_cast<milliseconds>(std::chrono::system_clock::now())); -+} -+ -+std::string -+PingContext::stateToString(const PingContext::State& state) { -+ std::string label = ""; -+ switch (state) { -+ case NEW: -+ label = "NEW"; -+ break; -+ case WAITING_TO_SEND: -+ label = "WAITING_TO_SEND"; -+ break; -+ case SENDING: -+ label = "SENDING"; -+ break; -+ case WAITING_FOR_REPLY: -+ label = "WAITING_FOR_REPLY"; -+ break; -+ case TARGET_FREE: -+ label = "TARGET_FREE"; -+ break; -+ case TARGET_IN_USE: -+ label = "TARGET_IN_USE"; -+ break; -+ } -+ -+ return (label); -+} -+ -+const IOAddress& PingContext::getTarget() const { -+ return (lease_->addr_); -+} -+ -+uint32_t -+PingContext::getMinEchos() const { -+ return (min_echos_); -+} -+ -+void -+PingContext::setMinEchos(uint32_t value) { -+ min_echos_ = value; -+} -+ -+uint32_t -+PingContext::getReplyTimeout() const { -+ return (reply_timeout_); -+} -+ -+void -+PingContext::setReplyTimeout(uint32_t value) { -+ reply_timeout_ = value; -+} -+ -+uint32_t -+PingContext::getEchosSent() const { -+ return (echos_sent_); -+} -+ -+void -+PingContext::setEchosSent(uint32_t value) { -+ echos_sent_ = value; -+} -+ -+const TimeStamp& -+PingContext::getLastEchoSentTime() const { -+ return (last_echo_sent_time_); -+} -+ -+void -+PingContext::setLastEchoSentTime(const TimeStamp& value) { -+ last_echo_sent_time_ = value; -+} -+ -+const TimeStamp& -+PingContext::getSendWaitStart() const { -+ return (send_wait_start_); -+} -+ -+bool -+PingContext::isWaitingToSend() const { -+ return (state_ == WAITING_TO_SEND); -+} -+ -+void -+PingContext::setSendWaitStart(const TimeStamp& value) { -+ send_wait_start_ = value; -+} -+ -+const TimeStamp& -+PingContext::getNextExpiry() const { -+ return (next_expiry_); -+} -+ -+bool -+PingContext::isWaitingForReply() const { -+ return (state_ == WAITING_FOR_REPLY); -+} -+ -+void -+PingContext::setNextExpiry(const TimeStamp& value) { -+ next_expiry_ = value; -+} -+ -+const TimeStamp& -+PingContext::getCreatedTime() const { -+ return (created_time_); -+} -+ -+PingContext::State -+PingContext::getState() const { -+ return (state_); -+} -+ -+void -+PingContext::setState(const PingContext::State& value) { -+ state_ = value; -+} -+ -+Pkt4Ptr -+PingContext::getQuery() const { -+ return (query_); -+} -+ -+Lease4Ptr -+PingContext::getLease() const { -+ return (lease_); -+} -+ -+void -+PingContext::beginWaitingToSend(const TimeStamp& begin_time /* = now() */) { -+ state_ = WAITING_TO_SEND; -+ send_wait_start_ = begin_time; -+} -+ -+void -+PingContext::beginWaitingForReply(const TimeStamp& begin_time /* = now() */) { -+ ++echos_sent_; -+ last_echo_sent_time_ = begin_time; -+ next_expiry_ = begin_time + milliseconds(reply_timeout_); -+ state_ = WAITING_FOR_REPLY; -+} -+ -+} // end of namespace ping_check -+} // end of namespace isc -+ -diff --git a/src/hooks/dhcp/ping_check/ping_context.h b/src/hooks/dhcp/ping_check/ping_context.h -new file mode 100644 -index 0000000000..2c5b704a04 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/ping_context.h -@@ -0,0 +1,280 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#ifndef PING_CONTEXT_H -+#define PING_CONTEXT_H -+ -+#include <dhcp/pkt4.h> -+#include <dhcpsrv/lease.h> -+#include <hooks/parking_lots.h> -+ -+#include <chrono> -+ -+namespace isc { -+namespace ping_check { -+ -+/// @brief Specifies the type for time stamps. -+using TimeStamp = std::chrono::time_point<std::chrono::system_clock>; -+ -+/// @brief Embodies the life cycle of a ping check test for a single address -+/// for a single DHCPDISCOVER. -+/// -+/// The class uses a state-model to direct the tasks needed to execute one -+/// or more ECHO REQUEST SEND/WAIT FOR REPLY cycles until the address is -+/// either deemed free to offer or in-use and should not be offered. The -+/// number of cycles conducted is dictated by the minimum number of echos -+/// (@c min_echos_) and whether or not either an ECHO REPLY or DESTINATION -+/// UNREACHABLE are received. -+class PingContext { -+public: -+ -+ /// @brief Defines PingContext life cycle states -+ enum State { -+ NEW, // Newly created -+ WAITING_TO_SEND, // Waiting to send next ECHO REQUEST -+ SENDING, // Next ECHO REQUEST is being sent -+ WAITING_FOR_REPLY, // ECHO REQUEST sent, Waiting for reply or timeout -+ TARGET_FREE, // Target has been deemed free to offer. -+ TARGET_IN_USE // Target has been deemed in-use, do not offer -+ }; -+ -+ /// @brief Converts a string to State -+ /// -+ /// @param state_str Upper case string label to convert -+ /// @return State value corresponding to the given string -+ /// -+ /// @throw BadValue if the string is not a valid state label -+ static State stringToState(const std::string& state_str); -+ -+ /// @brief Converts a State to a string -+ /// -+ /// @param state State to convert -+ /// @return string label corresponding to the given state -+ static std::string stateToString(const State& state); -+ -+ /// @brief Constructor -+ /// -+ /// @param lease pointer to the lease whose address needs to be checked -+ /// @param query DHCPDISCOVER that instigated the check -+ /// @param min_echos minimum number of ECHO REQUESTs sent without replies -+ /// received required to declare an address free to offer. Defaults to 1, -+ /// must be greater than zero. -+ /// @param reply_timeout maximum number of milliseconds to wait for an -+ /// ECHO REPLY after an ECHO REQUEST has been sent. Defaults to 100, -+ /// must be greater than 0. -+ /// @param parking_lot parking lot in which the query is parked. Defaults -+ /// to an empty pointer. -+ /// -+ /// @throw BadValue if either lease or query are empty, or if the lease -+ /// address is 0.0.0.0 -+ PingContext(isc::dhcp::Lease4Ptr& lease, isc::dhcp::Pkt4Ptr& query, -+ uint32_t min_echos = 1, uint32_t reply_timeout = 100, -+ isc::hooks::ParkingLotHandlePtr& parking_lot = EMPTY_LOT()); -+ -+ /// @brief Destructor -+ virtual ~PingContext() = default; -+ -+ /// @brief Fetches the current timestamp (UTC/milliseconds precision) -+ /// -+ /// @return current time as a TimeStamp -+ static TimeStamp now(); -+ -+ /// @brief Fetches an empty timestamp -+ /// -+ /// @return an empty TimeStamp -+ static const TimeStamp& EMPTY_TIME() { -+ static TimeStamp empty_time; -+ return (empty_time); -+ } -+ -+ /// @brief Fetches the minimum timestamp -+ /// -+ /// @return the minimum timestamp -+ static const TimeStamp& MIN_TIME() { -+ static TimeStamp min_time = std::chrono::system_clock::time_point::min(); -+ return (min_time); -+ } -+ -+ /// @brief Fetches an empty parking lot handle -+ /// -+ /// @return an empty ParkingLotHandlePtr -+ static hooks::ParkingLotHandlePtr& EMPTY_LOT() { -+ static hooks::ParkingLotHandlePtr empty_lot(0); -+ return (empty_lot); -+ } -+ -+ /// @brief Fetches the IP address that is under test. -+ /// -+ /// @return IP address as an IOAddress -+ const isc::asiolink::IOAddress& getTarget() const; -+ -+ /// @brief Fetches the minimum number of ECHO REQUESTs -+ /// -+ /// @return minimum number of echos as a uint32_t -+ uint32_t getMinEchos() const; -+ -+ /// @brief Sets the minimum number of ECHO REQUESTs -+ /// -+ /// @param value new value, must be greater than 0 -+ /// -+ /// @throw BadValue if the given value is 0 -+ void setMinEchos(uint32_t value); -+ -+ /// @brief Fetches the reply timeout (milliseconds) -+ /// -+ /// @return reply timeout as a unit32_t -+ uint32_t getReplyTimeout() const; -+ -+ /// @brief Sets the reply timeout -+ /// -+ /// @param value new value in milliseconds, must be greater than 0 -+ /// -+ /// @throw BadValue if the given value is 0. -+ void setReplyTimeout(uint32_t value); -+ -+ /// @brief Fetches the number of ECHO REQUESTs sent. -+ /// -+ /// @return number of echos sent as a unit32_t -+ uint32_t getEchosSent() const; -+ -+ /// @brief Sets the number of ECHO REQUESTs sent. -+ /// -+ /// @param value new value -+ void setEchosSent(uint32_t value); -+ -+ /// @brief Fetches the timestamp of when the most recent ECHO REQUEST -+ /// was sent -+ /// -+ /// @return time the last echo was sent as a TimeStamp -+ const TimeStamp& getLastEchoSentTime() const; -+ -+ /// @brief Sets the timestamp the most recent ECHO REQUEST was sent -+ /// -+ /// @param value new value -+ void setLastEchoSentTime(const TimeStamp& value); -+ -+ /// @brief Fetches the time the context went into WAITING_TO_SEND state -+ /// -+ /// The value returned is only meaningful when the context state is WAITING_TO_SEND. -+ /// -+ /// @return send waits start time as a TimeStamp -+ const TimeStamp& getSendWaitStart() const; -+ -+ /// @brief Sets the send wait start timestamp -+ /// -+ /// @param value new value -+ void setSendWaitStart(const TimeStamp& value); -+ -+ /// @brief Returns true if state is WAITING_TO_SEND -+ /// -+ /// @return True if the context is in WAITING_TO_SEND state -+ bool isWaitingToSend() const; -+ -+ /// @brief Fetches the time at which the WAITING_FOR_REPLY state expires(ed) -+ /// -+ /// The value returned is only meaningful when the context state is WAITING_FOR_REPLY. -+ /// -+ /// @return expiration -+ const TimeStamp& getNextExpiry() const; -+ -+ /// @brief Sets the timestamp which specifies the time at which the WAITING_FOR_REPLY state expires -+ /// @param value new value -+ void setNextExpiry(const TimeStamp& value); -+ -+ /// @brief Returns true if state is WAITING_FOR_REPLY -+ /// -+ /// @return True if the context is in WAITING_TO_REPLY state -+ bool isWaitingForReply() const; -+ -+ /// @brief Fetches the time at which the context was created -+ /// -+ /// @return creation time as a TimeStamp -+ const TimeStamp& getCreatedTime() const; -+ -+ /// @brief Fetches the current state. -+ /// -+ /// @return current state as PingContext::State -+ State getState() const; -+ -+ /// @brief Sets the state. -+ /// -+ /// @param value new state value -+ void setState(const State& value); -+ -+ /// @brief Returns the query that instigated this check -+ /// -+ /// @return query as a Pkt4Ptr -+ isc::dhcp::Pkt4Ptr getQuery() const; -+ -+ /// @brief Returns the candidate lease whose address is the target to check -+ /// -+ /// @return lease under test as a Lease4Ptr -+ isc::dhcp::Lease4Ptr getLease() const; -+ -+ /// @brief Enters WAITING_TO_SEND state -+ /// -+ /// @param begin_time timestamp of when the state began. Defaults to -+ /// time now. Provided for testing purposes. -+ void beginWaitingToSend(const TimeStamp& begin_time = PingContext::now()); -+ -+ /// @brief Enters WAITING_TO_REPLY state -+ /// -+ /// @param begin_time timestamp of when the state began. Defaults to -+ /// time now. Provided for testing purposes. -+ void beginWaitingForReply(const TimeStamp& begin_time = PingContext::now()); -+ -+ /// @brief Fetches the parking lot used for this context. -+ /// -+ /// @return Pointer to the parking lot handle or empty if parking is not -+ /// employed. -+ isc::hooks::ParkingLotHandlePtr getParkingLot() { -+ return (parking_lot_); -+ }; -+ -+private: -+ /// @brief Minimum number of echos to send without receiving a reply -+ /// before giving up -+ uint32_t min_echos_ = 0; -+ -+ /// @brief Amount of time (likely in ms) to wait for an echo reply -+ uint32_t reply_timeout_ = 0; -+ -+ /// @brief Number of echos sent since instantiation -+ uint32_t echos_sent_ = 0; -+ -+ /// @brief Timestamp the most recent echo send completed -+ TimeStamp last_echo_sent_time_; -+ -+ /// @brief Timestamp of entry into waiting_to_send -+ TimeStamp send_wait_start_; -+ -+ /// @brief Timestamp the most recent echo times out -+ TimeStamp next_expiry_; -+ -+ /// @brief Time context was created -+ TimeStamp created_time_; -+ -+ /// @brief Candidate lease to check -+ isc::dhcp::Lease4Ptr lease_; -+ -+ /// @brief DHCPDISCOVER packet that instigated this check. -+ isc::dhcp::Pkt4Ptr query_; -+ -+ /// @brief Current state of this context -+ State state_; -+ -+ /// @brief Parking lot where the associated query is parked. -+ /// If empty parking is not being employed. -+ isc::hooks::ParkingLotHandlePtr parking_lot_; -+}; -+ -+/// @brief Defines a shared pointer to a PingContext. -+typedef boost::shared_ptr<PingContext> PingContextPtr; -+ -+} // end of namespace ping_check -+} // end of namespace isc -+ -+#endif -diff --git a/src/hooks/dhcp/ping_check/ping_context_store.cc b/src/hooks/dhcp/ping_check/ping_context_store.cc -new file mode 100644 -index 0000000000..35712d5afe ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/ping_context_store.cc -@@ -0,0 +1,144 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#include <config.h> -+ -+#include <ping_context_store.h> -+#include <util/multi_threading_mgr.h> -+ -+using namespace std; -+using namespace isc; -+using namespace isc::asiolink; -+using namespace isc::dhcp; -+using namespace isc::hooks; -+using namespace isc::util; -+using namespace std::chrono; -+ -+namespace isc { -+namespace ping_check { -+ -+PingContextPtr -+PingContextStore::addContext(Lease4Ptr& lease, Pkt4Ptr& query, -+ uint32_t min_echos, uint32_t reply_timeout, -+ ParkingLotHandlePtr& parking_lot) { -+ -+ MultiThreadingLock lock(*mutex_); -+ PingContextPtr context; -+ try { -+ context.reset(new PingContext(lease, query, min_echos, reply_timeout, parking_lot)); -+ } catch (const std::exception& ex) { -+ isc_throw(BadValue, "PingContextStore::addContext failed: " << ex.what()); -+ } -+ -+ context->beginWaitingToSend(); -+ auto ret = pings_.insert(context); -+ if (ret.second == false) { -+ isc_throw(DuplicateContext, "PingContextStore::addContex: context already exists for: " -+ << lease->addr_); -+ } -+ -+ return (context); -+} -+ -+void -+PingContextStore::updateContext(const PingContextPtr& context) { -+ MultiThreadingLock lock(*mutex_); -+ auto& index = pings_.get<AddressIndexTag>(); -+ auto context_iter = index.find(context->getTarget()); -+ if (context_iter == index.end()) { -+ isc_throw(InvalidOperation, "PingContextStore::updateContext failed for address: " -+ << context->getTarget() << ", not in store"); -+ } -+ -+ // Use replace() to re-index contexts. -+ index.replace(context_iter, PingContextPtr(new PingContext(*context))); -+} -+ -+void -+PingContextStore::deleteContext(const PingContextPtr& context) { -+ MultiThreadingLock lock(*mutex_); -+ auto& index = pings_.get<AddressIndexTag>(); -+ auto context_iter = index.find(context->getTarget()); -+ if (context_iter == index.end()) { -+ // Not there, just return. -+ return; -+ } -+ -+ // Remove the context from the store. -+ pings_.erase(context_iter); -+} -+ -+PingContextPtr -+PingContextStore::getContextByAddress(const IOAddress& address) { -+ MultiThreadingLock lock(*mutex_); -+ auto const& index = pings_.get<AddressIndexTag>(); -+ auto context_iter = index.find(address); -+ return (context_iter == index.end() ? PingContextPtr() -+ : PingContextPtr(new PingContext(**context_iter))); -+} -+ -+PingContextPtr -+PingContextStore::getContextByQuery(Pkt4Ptr& query) { -+ MultiThreadingLock lock(*mutex_); -+ auto const& index = pings_.get<QueryIndexTag>(); -+ auto context_iter = index.find(query); -+ return (context_iter == index.end() ? PingContextPtr() -+ : PingContextPtr(new PingContext(**context_iter))); -+} -+ -+PingContextPtr -+PingContextStore::getNextToSend() { -+ MultiThreadingLock lock(*mutex_); -+ auto const& index = pings_.get<NextToSendIndexTag>(); -+ auto context_iter = index.lower_bound(boost::make_tuple(true, PingContext::MIN_TIME())); -+ return (context_iter == index.end() ? PingContextPtr() -+ : PingContextPtr(new PingContext(**context_iter))); -+} -+ -+PingContextPtr -+PingContextStore::getExpiresNext() { -+ MultiThreadingLock lock(*mutex_); -+ auto const& index = pings_.get<ExpirationIndexTag>(); -+ auto context_iter = index.lower_bound(boost::make_tuple(true, PingContext::now() + milliseconds(1))); -+ return (context_iter == index.end() ? PingContextPtr() -+ : PingContextPtr(new PingContext(**context_iter))); -+} -+ -+PingContextCollectionPtr -+PingContextStore::getExpiredSince(const TimeStamp& since) { -+ MultiThreadingLock lock(*mutex_); -+ auto const& index = pings_.get<ExpirationIndexTag>(); -+ auto lower_limit = index.lower_bound(boost::make_tuple(true, PingContext::MIN_TIME())); -+ auto upper_limit = index.upper_bound(boost::make_tuple(true, since)); -+ -+ PingContextCollectionPtr collection(new PingContextCollection()); -+ for (auto context_iter = lower_limit; context_iter != upper_limit; ++context_iter) { -+ PingContextPtr context(new PingContext(**context_iter)); -+ collection->push_back(context); -+ } -+ -+ return (collection); -+} -+ -+PingContextCollectionPtr -+PingContextStore::getAll() { -+ MultiThreadingLock lock(*mutex_); -+ auto const& index = pings_.get<AddressIndexTag>(); -+ PingContextCollectionPtr collection(new PingContextCollection()); -+ for (auto const& context_iter : index) { -+ collection->push_back(PingContextPtr(new PingContext(*context_iter))); -+ } -+ -+ return (collection); -+} -+ -+void PingContextStore::clear() { -+ MultiThreadingLock lock(*mutex_); -+ pings_.clear(); -+} -+ -+} // end of namespace ping_check -+} // end of namespace isc -diff --git a/src/hooks/dhcp/ping_check/ping_context_store.h b/src/hooks/dhcp/ping_check/ping_context_store.h -new file mode 100644 -index 0000000000..3a7664bfca ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/ping_context_store.h -@@ -0,0 +1,240 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#ifndef PING_CONTEXT_STORE_H -+#define PING_CONTEXT_STORE_H -+ -+#include <asiolink/io_address.h> -+#include <ping_context.h> -+ -+#include <boost/multi_index/indexed_by.hpp> -+#include <boost/multi_index/member.hpp> -+#include <boost/multi_index/mem_fun.hpp> -+#include <boost/multi_index/ordered_index.hpp> -+#include <boost/multi_index_container.hpp> -+#include <boost/multi_index/composite_key.hpp> -+#include <boost/scoped_ptr.hpp> -+ -+#include <mutex> -+#include <vector> -+ -+namespace isc { -+namespace ping_check { -+ -+/// @brief Exception thrown when an attempt was made to add a duplicate context -+class DuplicateContext : public Exception { -+public: -+ DuplicateContext(const char* file, size_t line, const char* what) : -+ isc::Exception(file, line, what) {} -+}; -+ -+/// @brief Tag for index by target address. -+struct AddressIndexTag { }; -+ -+/// @brief Tag for index by the query packet. -+struct QueryIndexTag { }; -+ -+/// @brief Tag for index by send wait start time. -+struct NextToSendIndexTag { }; -+ -+/// @brief Tag for index by expiration time. -+struct ExpirationIndexTag { }; -+ -+/// @brief Tag for index by state. -+struct StateIndexTag { }; -+ -+/// @brief A multi index container holding pointers to PingContexts. -+/// -+/// The contexts in the container may be accessed using different indexes: -+/// - using an IPv4 address, -+/// - using a query packet -+/// - using a send wait start time -+/// - using an expiration time -+/// - using a context state -+/// -+/// Indexes can be accessed using the index number (from 0 to 2) or a -+/// name tag. It is recommended to use the tags to access indexes as -+/// they do not depend on the order of indexes in the container. -+typedef boost::multi_index_container< -+ // It holds pointers to Lease6 objects. -+ PingContextPtr, -+ boost::multi_index::indexed_by< -+ // Specification of the first index starts here. -+ // This index sorts PingContexts by IPv4 addresses represented as -+ // IOAddress objects. -+ /// @todo Does it need to be ordered or only unique? -+ boost::multi_index::ordered_unique< -+ boost::multi_index::tag<AddressIndexTag>, -+ boost::multi_index::const_mem_fun<PingContext, const isc::asiolink::IOAddress&, -+ &PingContext::getTarget> -+ >, -+ -+ // Specification of the second index starts here. -+ // This index sorts contexts by query. -+ boost::multi_index::ordered_unique< -+ boost::multi_index::tag<QueryIndexTag>, -+ boost::multi_index::const_mem_fun<PingContext, isc::dhcp::Pkt4Ptr, -+ &PingContext::getQuery> -+ >, -+ -+ // Specification of the third index starts here. -+ // This index sorts contexts by send_wait_start. -+ boost::multi_index::ordered_non_unique< -+ boost::multi_index::tag<NextToSendIndexTag>, -+ boost::multi_index::composite_key< -+ PingContext, -+ // The boolean value specifying if context is waiting to send -+ boost::multi_index::const_mem_fun<PingContext, bool, -+ &PingContext::isWaitingToSend>, -+ // Context expiration time. -+ boost::multi_index::const_mem_fun<PingContext, const TimeStamp&, -+ &PingContext::getSendWaitStart> -+ > -+ >, -+ -+ // Specification of the fourth index starts here. -+ // This index sorts contexts by next_expiry. -+ boost::multi_index::ordered_non_unique< -+ boost::multi_index::tag<ExpirationIndexTag>, -+ boost::multi_index::composite_key< -+ PingContext, -+ // The boolean value specifying if context is waiting for a reply -+ boost::multi_index::const_mem_fun<PingContext, bool, -+ &PingContext::isWaitingForReply>, -+ // Context expiration time. -+ boost::multi_index::const_mem_fun<PingContext, const TimeStamp&, -+ &PingContext::getNextExpiry> -+ > -+ >, -+ -+ // Specification of the fifth index starts here. -+ // This index sorts contexts by State. -+ boost::multi_index::ordered_non_unique< -+ boost::multi_index::tag<StateIndexTag>, -+ boost::multi_index::const_mem_fun<PingContext, PingContext::State, -+ &PingContext::getState> -+ > -+ > -+> PingContextContainer; -+ -+/// @brief Type for a collection of PingContextPtrs. -+typedef std::vector<PingContextPtr> PingContextCollection; -+/// @brief Type for a pointer to a collection of PingContextPtrs. -+typedef boost::shared_ptr<PingContextCollection> PingContextCollectionPtr; -+ -+/// @brief Maintains an in-memory store of PingContexts -+/// -+/// Provides essential CRUD functions for managing a collection of -+/// PingContexts. Additionally there are finders that can return -+/// contexts by target IP address, instigating query, WAITING_TO_SEND -+/// start time, WAITING_FOR_REPLY expiration time, and context state. -+/// All finders return copies of the contexts found, rather than the -+/// stored context itself. -+class PingContextStore { -+public: -+ -+ /// @brief Constructor -+ PingContextStore() : pings_(), mutex_(new std::mutex) { -+ } -+ -+ /// @brief Destructor -+ ~PingContextStore() = default; -+ -+ /// @brief Creates a new PingContext and adds it to the store -+ /// -+ /// @param lease lease whose address is to be ping checked -+ /// @param query query that instigated the lease -+ /// @param min_echos minimum number of ECHO REQUESTs sent without replies -+ /// received required to declare an address free to offer. Must be -+ /// greater than zero. -+ /// @param reply_timeout maximum number of milliseconds to wait for an -+ /// ECHO REPLY after an ECHO REQUEST has been sent. Must be greater than 0. -+ /// @param parking_lot parking lot in which query is parked. If empty, -+ /// parking is assumed to not be employed. -+ /// -+ /// @return pointer to the newly created context -+ /// @throw DuplicateContext is a context for the lease address already -+ /// exists in the store. -+ PingContextPtr addContext(isc::dhcp::Lease4Ptr& lease, -+ isc::dhcp::Pkt4Ptr& query, -+ uint32_t min_echos, -+ uint32_t reply_timeout, -+ isc::hooks::ParkingLotHandlePtr& parking_lot -+ = PingContext::EMPTY_LOT()); -+ -+ /// @brief Updates a context in the store. -+ /// -+ /// The context is assumed to already exist in the store. -+ /// -+ /// @param context context to update. -+ /// -+ /// @throw InvalidOperation if PingContext does not exist in the store. -+ void updateContext(const PingContextPtr& context); -+ -+ /// @brief Removes the context from the store. -+ /// -+ /// If the context does not exist in the store, it simply returns. -+ /// -+ /// @param context context to delete. -+ void deleteContext(const PingContextPtr& context); -+ -+ /// @brief Fetches the context with a given target address -+ /// -+ /// @param address target IP address for which to search -+ /// -+ /// @return pointer to the matching PingContext or an empty pointer if -+ /// not found. -+ PingContextPtr getContextByAddress(const isc::asiolink::IOAddress& address); -+ -+ /// @brief Fetches the context with a given query packet -+ /// -+ /// @param query query for which to search -+ /// -+ /// @return pointer to the matching PingContext or an empty pointer if -+ /// not found. -+ PingContextPtr getContextByQuery(isc::dhcp::Pkt4Ptr& query); -+ -+ /// @brief Fetches the context in WAITING_TO_SEND with the oldest send wait -+ /// start time. -+ /// -+ /// @return pointer to the matching PingContext or an empty pointer if -+ /// not found. -+ PingContextPtr getNextToSend(); -+ -+ /// @brief Fetches the context in WAITING_FOR_REPLY with the oldest expiration -+ /// time that has not already passed (i.e. is still in the future) -+ /// -+ /// @return pointer to the matching PingContext or an empty pointer if -+ /// not found. -+ PingContextPtr getExpiresNext(); -+ -+ /// @brief Fetches the contexts in WAITING_FOR_REPLY that expired since a given time -+ /// -+ /// @param since timestamp to search by. Defaults to current time. -+ /// -+ /// @return a collection of the matching contexts, ordered by expiration time. -+ PingContextCollectionPtr getExpiredSince(const TimeStamp& since = PingContext::now()); -+ -+ /// @brief Fetches all of the contexts (in order by target) -+ /// -+ /// @return a collection of all contexts in the store. -+ PingContextCollectionPtr getAll(); -+ -+ /// @brief Removes all contexts from the store. -+ void clear(); -+ -+private: -+ /// @brief Container instance. -+ PingContextContainer pings_; -+ -+ /// @brief The mutex used to protect internal state. -+ const boost::scoped_ptr<std::mutex> mutex_; -+}; -+ -+} // end of namespace ping_check -+} // end of namespace isc -+ -+#endif -diff --git a/src/hooks/dhcp/ping_check/tests/.gitignore b/src/hooks/dhcp/ping_check/tests/.gitignore -new file mode 100644 -index 0000000000..7e12f9e5be ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/tests/.gitignore -@@ -0,0 +1 @@ -+ping_check_unittests -diff --git a/src/hooks/dhcp/ping_check/tests/Makefile.am b/src/hooks/dhcp/ping_check/tests/Makefile.am -new file mode 100644 -index 0000000000..a8c2ea4d92 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/tests/Makefile.am -@@ -0,0 +1,70 @@ -+SUBDIRS = . -+ -+AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib -+AM_CPPFLAGS += -I$(top_builddir)/src/hooks/dhcp/ping_check -I$(top_srcdir)/src/hooks/dhcp/ping_check -+AM_CPPFLAGS += $(BOOST_INCLUDES) $(CRYPTO_CFLAGS) $(CRYPTO_INCLUDES) -+AM_CPPFLAGS += -DPING_CHECK_LIB_SO=\"$(abs_top_builddir)/src/hooks/dhcp/ping_check/.libs/libdhcp_ping_check.so\" -+AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\" -+ -+AM_CXXFLAGS = $(KEA_CXXFLAGS) -+ -+if USE_STATIC_LINK -+AM_LDFLAGS = -static -+endif -+ -+# Unit test data files need to get installed. -+EXTRA_DIST = -+ -+CLEANFILES = *.gcno *.gcda -+ -+TESTS_ENVIRONMENT = $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND) -+ -+LOG_COMPILER = $(LIBTOOL) -+AM_LOG_FLAGS = --mode=execute -+ -+TESTS = -+if HAVE_GTEST -+TESTS += ping_check_unittests -+ -+ping_check_unittests_SOURCES = run_unittests.cc -+ping_check_unittests_SOURCES += icmp_endpoint_unittests.cc -+ping_check_unittests_SOURCES += icmp_socket_unittests.cc -+ping_check_unittests_SOURCES += ping_context_unittests.cc -+ping_check_unittests_SOURCES += ping_context_store_unittests.cc -+ping_check_unittests_SOURCES += icmp_msg_unittests.cc -+ping_check_unittests_SOURCES += ping_test_utils.h -+ping_check_unittests_SOURCES += ping_channel_unittests.cc -+ping_check_unittests_SOURCES += ping_check_mgr_unittests.cc -+ping_check_unittests_SOURCES += ping_check_config_unittests.cc -+ping_check_unittests_SOURCES += config_cache_unittests.cc -+ -+ping_check_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) $(LOG4CPLUS_INCLUDES) -+ -+ping_check_unittests_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) $(GTEST_LDFLAGS) -+ -+ping_check_unittests_CXXFLAGS = $(AM_CXXFLAGS) -+ -+ping_check_unittests_LDADD = $(top_builddir)/src/hooks/dhcp/ping_check/libping_check.la -+ping_check_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la -+ping_check_unittests_LDADD += $(top_builddir)/src/lib/process/libkea-process.la -+ping_check_unittests_LDADD += $(top_builddir)/src/lib/eval/libkea-eval.la -+ping_check_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la -+ping_check_unittests_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la -+ping_check_unittests_LDADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la -+ping_check_unittests_LDADD += $(top_builddir)/src/lib/http/libkea-http.la -+ping_check_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la -+ping_check_unittests_LDADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la -+ping_check_unittests_LDADD += $(top_builddir)/src/lib/database/libkea-database.la -+ping_check_unittests_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la -+ping_check_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la -+ping_check_unittests_LDADD += $(top_builddir)/src/lib/dns/libkea-dns++.la -+ping_check_unittests_LDADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la -+ping_check_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la -+ping_check_unittests_LDADD += $(top_builddir)/src/lib/util/libkea-util.la -+ping_check_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la -+ping_check_unittests_LDADD += $(LOG4CPLUS_LIBS) -+ping_check_unittests_LDADD += $(CRYPTO_LIBS) -+ping_check_unittests_LDADD += $(BOOST_LIBS) -+ping_check_unittests_LDADD += $(GTEST_LDADD) -+endif -+noinst_PROGRAMS = $(TESTS) -diff --git a/src/hooks/dhcp/ping_check/tests/config_cache_unittests.cc b/src/hooks/dhcp/ping_check/tests/config_cache_unittests.cc -new file mode 100644 -index 0000000000..f4e48d6591 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/tests/config_cache_unittests.cc -@@ -0,0 +1,245 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+/// @file This file contains tests which verify the subnet ping-check -+/// configuration cache. -+ -+#include <config.h> -+#include <config_cache.h> -+#include <dhcpsrv/cfgmgr.h> -+#include <hooks/callout_manager.h> -+#include <hooks/hooks.h> -+#include <testutils/gtest_utils.h> -+#include <testutils/multi_threading_utils.h> -+ -+#include <boost/date_time/posix_time/posix_time.hpp> -+#include <gtest/gtest.h> -+#include <sstream> -+ -+using namespace std; -+using namespace isc; -+using namespace isc::data; -+using namespace isc::dhcp; -+using namespace isc::hooks; -+using namespace isc::ping_check; -+using namespace isc::test; -+using namespace boost::posix_time; -+ -+namespace { -+ -+/// @brief ConfigCache derivation that allows flush time to be modified. -+class TestConfigCache : public ConfigCache { -+public: -+ /// @brief Constructor -+ TestConfigCache() { -+ } -+ -+ /// @brief Destructor -+ virtual ~TestConfigCache() { -+ } -+ -+ /// @brief Adjusts the last flush time by the given amount. -+ /// -+ /// @param offset signed value in seconds to add to cache's last -+ /// flush time value -+ void tweakLastFlushTime(int offset) { -+ setModificationTime(getLastFlushTime() + seconds(offset)); -+ } -+}; -+ -+/// @brief Test fixture for testing ConfigCache. -+class ConfigCacheTest : public ::testing::Test { -+public: -+ /// @brief Constructor -+ ConfigCacheTest() { -+ isc::util::MultiThreadingMgr::instance().setMode(false); -+ } -+ -+ /// @brief Destructor -+ virtual ~ConfigCacheTest() { -+ } -+ -+ /// @brief Verifies construction of a ConfigCache. -+ void testConstruction() { -+ // We use a BaseStampedElement to get the current time to ensure we -+ // are using the same time perspective (currently local) as StampedElements do. -+ BaseStampedElement now; -+ ptime start_time = now.getModificationTime(); -+ -+ // Create a new cache. -+ TestConfigCache configs; -+ EXPECT_EQ(configs.size(), 0); -+ -+ // Verify that last_flush_time_ has been set and that the -+ // cache has no entries. -+ ptime last_flush_time = configs.getLastFlushTime(); -+ EXPECT_GE(last_flush_time, start_time); -+ -+ // Verify that looking for an entry in an empty cache -+ // gracefully finds nothing. -+ PingCheckConfigPtr fetched_config; -+ EXPECT_FALSE(configs.findConfig(999, fetched_config)); -+ EXPECT_FALSE(fetched_config); -+ } -+ -+ /// @brief Verifies that invalid user-context config is rejected gracefully. -+ void testInvalidConfig() { -+ // Create a new cache. -+ TestConfigCache configs; -+ EXPECT_EQ(configs.size(), 0); -+ -+ // An invalid keyword should fail. -+ std::string json = -+ R"({ -+ "ping-check" : { -+ "bogus" : 777 -+ } -+ })"; -+ -+ ConstElementPtr user_context; -+ ASSERT_NO_THROW_LOG(user_context = Element::fromJSON(json)); -+ -+ ASSERT_THROW_MSG(configs.parseAndCacheConfig(1, user_context), DhcpConfigError, -+ "spurious 'bogus' parameter"); -+ -+ EXPECT_EQ(configs.size(), 0); -+ } -+ -+ /// @brief Verifies that valid user-context supplied config are cached correctly. -+ void testValidConfig() { -+ // Create a new cache. -+ TestConfigCache configs; -+ EXPECT_EQ(configs.size(), 0); -+ -+ // A valid config should get cached. -+ std::string json = -+ R"({ -+ "ping-check" : { -+ "enable-ping-check" : false, -+ "min-ping-requests" : 2, -+ "reply-timeout" : 375, -+ "ping-cltt-secs" : 120, -+ "ping-channel-threads" : 6 -+ } -+ })"; -+ -+ ConstElementPtr user_context; -+ ASSERT_NO_THROW_LOG(user_context = Element::fromJSON(json)); -+ -+ // Verify that we cache a valid config. -+ PingCheckConfigPtr config; -+ ASSERT_NO_THROW_LOG(config = configs.parseAndCacheConfig(1, user_context)); -+ ASSERT_TRUE(config); -+ EXPECT_EQ(configs.size(), 1); -+ -+ // Verify we can retrieve the cached config. -+ PingCheckConfigPtr fetched_config; -+ ASSERT_TRUE(configs.findConfig(1, fetched_config)); -+ EXPECT_EQ(fetched_config, config); -+ } -+ -+ /// @brief Verifies that an empty config pointer can be cached. -+ void testConfigCacheEmptyConfig() { -+ // Create a new cache. -+ TestConfigCache configs; -+ EXPECT_EQ(configs.size(), 0); -+ -+ // Verify that we can cache an empty config pointer. -+ PingCheckConfigPtr no_config; -+ ASSERT_NO_THROW_LOG(configs.cacheConfig(1, no_config)); -+ EXPECT_EQ(configs.size(), 1); -+ -+ // Verify we can retrieve the cached empty config pointer. -+ PingCheckConfigPtr fetched_config; -+ ASSERT_TRUE(configs.findConfig(1, fetched_config)); -+ ASSERT_FALSE(fetched_config); -+ } -+ -+ /// @brief Verifies that the cache can be cleared correctly. -+ void testFlushCache() { -+ // Create a new cache. -+ TestConfigCache configs; -+ EXPECT_EQ(configs.size(), 0); -+ -+ ptime last_flush_time = configs.getLastFlushTime(); -+ -+ // Now let's wind the clock back on last_flush_time. -+ configs.tweakLastFlushTime(-1000); -+ EXPECT_LT(configs.getLastFlushTime(), last_flush_time); -+ last_flush_time = configs.getLastFlushTime(); -+ -+ // Make a simple valid config. -+ std::string json = -+ R"({ -+ "ping-check": { -+ "enable-ping-check" : true -+ } -+ })"; -+ -+ ConstElementPtr user_context; -+ ASSERT_NO_THROW_LOG(user_context = Element::fromJSON(json)); -+ -+ for (int id = 1; id < 5; ++id) { -+ PingCheckConfigPtr config; -+ ASSERT_NO_THROW_LOG(config = configs.parseAndCacheConfig(id, user_context)); -+ ASSERT_TRUE(config); -+ EXPECT_EQ(configs.size(), id); -+ } -+ -+ // Verify we can explicitly clear the cache. Should be no entries -+ // and last_flush_time should be updated. -+ configs.flush(); -+ EXPECT_GT(configs.getLastFlushTime(), last_flush_time); -+ EXPECT_EQ(configs.size(), 0); -+ } -+}; -+ -+TEST_F(ConfigCacheTest, construction) { -+ testConstruction(); -+} -+ -+TEST_F(ConfigCacheTest, constructionMultiThreading) { -+ MultiThreadingTest mt; -+ testConstruction(); -+} -+ -+TEST_F(ConfigCacheTest, invalidConfig) { -+ testInvalidConfig(); -+} -+ -+TEST_F(ConfigCacheTest, invalidConfigMultiThreading) { -+ MultiThreadingTest mt; -+ testInvalidConfig(); -+} -+ -+TEST_F(ConfigCacheTest, validConfig) { -+ testValidConfig(); -+} -+ -+TEST_F(ConfigCacheTest, validConfigMultiThreading) { -+ MultiThreadingTest mt; -+ testValidConfig(); -+} -+ -+TEST_F(ConfigCacheTest, configCacheEmptyConfig) { -+ testConfigCacheEmptyConfig(); -+} -+ -+TEST_F(ConfigCacheTest, configCacheEmptyConfigMultiThreading) { -+ MultiThreadingTest mt; -+ testConfigCacheEmptyConfig(); -+} -+ -+TEST_F(ConfigCacheTest, flushCache) { -+ testFlushCache(); -+} -+ -+TEST_F(ConfigCacheTest, flushCacheMultiThreading) { -+ MultiThreadingTest mt; -+ testFlushCache(); -+} -+ -+} // end of anonymous namespace -diff --git a/src/hooks/dhcp/ping_check/tests/icmp_endpoint_unittests.cc b/src/hooks/dhcp/ping_check/tests/icmp_endpoint_unittests.cc -new file mode 100644 -index 0000000000..e9ed8dcb9b ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/tests/icmp_endpoint_unittests.cc -@@ -0,0 +1,44 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#include <config.h> -+#include <asiolink/asio_wrapper.h> -+#include <asiolink/io_address.h> -+#include <icmp_endpoint.h> -+ -+#include <gtest/gtest.h> -+ -+#include <string> -+ -+using namespace isc::asiolink; -+using namespace isc::ping_check; -+using namespace std; -+ -+// This test checks that the endpoint can manage its own internal -+// boost::asio::ip::icmp::endpoint object for IPv4. -+TEST(ICMPEndpointTest, v4Address) { -+ const string test_address("192.0.2.1"); -+ -+ IOAddress address(test_address); -+ ICMPEndpoint endpoint(address); -+ -+ EXPECT_TRUE(address == endpoint.getAddress()); -+ EXPECT_EQ(static_cast<short>(IPPROTO_ICMP), endpoint.getProtocol()); -+ EXPECT_EQ(AF_INET, endpoint.getFamily()); -+} -+ -+// This test checks that the endpoint can manage its own internal -+// boost::asio::ip::icmp::endpoint object for IPv6. -+TEST(ICMPEndpointTest, v6Address) { -+ const string test_address("2001:db8::1235"); -+ -+ IOAddress address(test_address); -+ ICMPEndpoint endpoint(address); -+ -+ EXPECT_TRUE(address == endpoint.getAddress()); -+ EXPECT_EQ(static_cast<short>(IPPROTO_ICMPV6), endpoint.getProtocol()); -+ EXPECT_EQ(AF_INET6, endpoint.getFamily()); -+} -diff --git a/src/hooks/dhcp/ping_check/tests/icmp_msg_unittests.cc b/src/hooks/dhcp/ping_check/tests/icmp_msg_unittests.cc -new file mode 100644 -index 0000000000..36c7056840 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/tests/icmp_msg_unittests.cc -@@ -0,0 +1,172 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+/// @file This file contains tests which exercise the ICMPMsg class. -+ -+#include <config.h> -+#include <icmp_msg.h> -+#include <asiolink/io_address.h> -+#include <testutils/gtest_utils.h> -+#include <util/str.h> -+ -+#include <gtest/gtest.h> -+#include <list> -+ -+using namespace std; -+using namespace isc; -+using namespace isc::asiolink; -+using namespace isc::ping_check; -+ -+namespace { -+ -+// Verifies accessors. -+TEST(ICMPMsgTest, basics) { -+ ICMPMsgPtr msg(new ICMPMsg()); -+ -+ msg->setType(ICMPMsg::ECHO_REPLY); -+ EXPECT_EQ(ICMPMsg::ECHO_REPLY, msg->getType()); -+ -+ msg->setCode(77); -+ EXPECT_EQ(77, msg->getCode()); -+ -+ msg->setChecksum(0x8899); -+ EXPECT_EQ(0x8899, msg->getChecksum()); -+ -+ msg->setId(0x1122); -+ EXPECT_EQ(0x1122, msg->getId()); -+ -+ msg->setSequence(0x3344); -+ EXPECT_EQ(0x3344, msg->getSequence()); -+ -+ msg->setSource(IOAddress("192.0.2.1")); -+ EXPECT_EQ(IOAddress("192.0.2.1"), msg->getSource()); -+ -+ msg->setDestination(IOAddress("192.0.2.2")); -+ EXPECT_EQ(IOAddress("192.0.2.2"), msg->getDestination()); -+ -+ std::vector<uint8_t> payload{ 0x55, 0x66, 0x77, 0x88, 0x99 }; -+ msg->setPayload(payload.data(), payload.size()); -+ EXPECT_EQ(payload, msg->getPayload()); -+} -+ -+// Verifies that a valid ECHO REPLY message can be unpacked. -+TEST(ICMPMsgTest, unpackValidEchoReply) { -+ // Create wire data for a valid ECHO REPLY. -+ std::string echo_reply = -+ "45:00:00:30:73:8a:00:00:40:01:a0:ff:b2:10:01:19:b2:10:01:0a:" -+ "00:00:33:11:55:66:77:88:" -+ "00:00:00:00:00:00:00:00:" -+ "00:00:00:00:00:00:00:00:" -+ "00:00:00:00"; -+ -+ std::vector<uint8_t> wire_data; -+ ASSERT_NO_THROW_LOG(util::str::decodeSeparatedHexString(echo_reply, ":", wire_data)); -+ -+ // Unpack the wire data. -+ ICMPMsgPtr msg; -+ ASSERT_NO_THROW_LOG(msg = ICMPMsg::unpack(wire_data.data(), wire_data.size())); -+ ASSERT_TRUE(msg); -+ -+ // Verify the reply contents. -+ EXPECT_EQ(ICMPMsg::ECHO_REPLY, msg->getType()); -+ EXPECT_EQ(0, msg->getCode()); -+ EXPECT_EQ(0x3311, msg->getChecksum()); -+ EXPECT_EQ(0x5566, msg->getId()); -+ EXPECT_EQ(0x7788, msg->getSequence()); -+ EXPECT_EQ(IOAddress("178.16.1.25"), msg->getSource()); -+ EXPECT_EQ(IOAddress("178.16.1.10"), msg->getDestination()); -+ -+ std::vector<uint8_t> payload{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; -+ EXPECT_EQ(payload, msg->getPayload()); -+} -+ -+// Verifies that a valid DESTINATION UNREACHABLE message can be unpacked. -+TEST(ICMPMsgTest, unpackValidUnreachable) { -+ // Valid destination unreachable message. Payload is the original -+ // ECHO request. -+ std::string unreachable = -+ "45:c0:00:4c:31:b3:00:00:40:01:e2:09:b2:10:01:0a:b2:10:01:0a:" -+ "03:01:fc:fe:00:00:00:00:" -+ "45:00:00:30:e3:e2:40:00:40:01:f0:5c:" -+ "b2:10:01:0a:b2:10:01:63:08:00:2b:11:" -+ "55:66:77:88:00:00:00:00:00:00:00:00:" -+ "00:00:00:00:00:00:00:00:00:00:00:00"; -+ -+ // Create the wire data. -+ std::vector<uint8_t> wire_data; -+ ASSERT_NO_THROW_LOG(util::str::decodeSeparatedHexString(unreachable, ":", wire_data)); -+ -+ // Unpack the outer message. -+ ICMPMsgPtr msg; -+ ASSERT_NO_THROW_LOG(msg = ICMPMsg::unpack(wire_data.data(), wire_data.size())); -+ ASSERT_TRUE(msg); -+ -+ // Verify its contents. -+ EXPECT_EQ(ICMPMsg::TARGET_UNREACHABLE, msg->getType()); -+ EXPECT_EQ(1, msg->getCode()); -+ EXPECT_EQ(0xfcfe, msg->getChecksum()); -+ EXPECT_EQ(0, msg->getId()); -+ EXPECT_EQ(0, msg->getSequence()); -+ EXPECT_EQ(IOAddress("178.16.1.10"), msg->getSource()); -+ EXPECT_EQ(IOAddress("178.16.1.10"), msg->getDestination()); -+ -+ // Now unpack the original ECHO from the outer message payload. -+ std::vector<uint8_t> payload(wire_data.begin() + 28, wire_data.end()); -+ EXPECT_EQ(payload, msg->getPayload()); -+ -+ ICMPMsgPtr payload_msg; -+ ASSERT_NO_THROW_LOG(payload_msg = ICMPMsg::unpack(payload.data(), payload.size())); -+ ASSERT_TRUE(payload_msg); -+ -+ // Verify the original ECHO contents. -+ EXPECT_EQ(ICMPMsg::ECHO_REQUEST, payload_msg->getType()); -+ EXPECT_EQ(0, payload_msg->getCode()); -+ EXPECT_EQ(0x2b11, payload_msg->getChecksum()); -+ EXPECT_EQ(0x5566, payload_msg->getId()); -+ EXPECT_EQ(0x7788, payload_msg->getSequence()); -+ EXPECT_EQ(IOAddress("178.16.1.10"), payload_msg->getSource()); -+ EXPECT_EQ(IOAddress("178.16.1.99"), payload_msg->getDestination()); -+} -+ -+// Verifies the malformed packets are detected. -+TEST(ICMPMsgTest, unpackInValidPackets) { -+ // Contains a test scenario. -+ struct Scenario { -+ // Wire data to submit to unpack. -+ std::string wire_data_; -+ // Expected exception message. -+ std::string error_msg_; -+ }; -+ -+ // List of scenarios to test. -+ std::list<Scenario> scenarios = { -+ { -+ // Truncated IP header -+ "45:c0:00:4c:31:b3:00:00:40:01:e2:09:b2", -+ "ICMPMsg::unpack - truncated ip header, length: 13" -+ }, -+ { -+ // Truncated packet -+ "45:c0:00:4c:31:b3:00:00:40:01:e2:09:b2:10:01:0a:b2:10:01:0a:" -+ "03:01:fc:fe:00:00:00:00:" -+ "45:00:00:30:e3:e2:40:00:40:01:f0:5c", -+ "ICMPMsg::truncated packet? length: 40, hlen: 20" -+ } -+ -+ }; -+ -+ // Iterate over scenarios. -+ for (auto const& scenario : scenarios) { -+ // Create the wire data. -+ std::vector<uint8_t> wire_data; -+ ASSERT_NO_THROW_LOG(util::str::decodeSeparatedHexString(scenario.wire_data_, ":", wire_data)); -+ ASSERT_THROW_MSG(ICMPMsg::unpack(wire_data.data(), wire_data.size()), BadValue, scenario.error_msg_); -+ } -+} -+ -+/// @todo YOU NEED some round trip tests that test packing! -+ -+} // end of anonymous namespace -diff --git a/src/hooks/dhcp/ping_check/tests/icmp_socket_unittests.cc b/src/hooks/dhcp/ping_check/tests/icmp_socket_unittests.cc -new file mode 100644 -index 0000000000..2394b360ca ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/tests/icmp_socket_unittests.cc -@@ -0,0 +1,380 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+/// \brief Test of ICMPSocket -+/// -+/// Tests the functionality of a ICMPSocket by working through an open-send- -+/// receive-close sequence and checking that the asynchronous notifications -+/// work. -+ -+#include <config.h> -+#include <asiolink/asio_wrapper.h> -+#include <asiolink/interval_timer.h> -+#include <asiolink/io_address.h> -+#include <asiolink/io_service.h> -+#include <icmp_socket.h> -+#include <icmp_msg.h> -+#include <exceptions/exceptions.h> -+#include <util/buffer.h> -+#include <testutils/gtest_utils.h> -+ -+#include <boost/shared_ptr.hpp> -+#include <boost/enable_shared_from_this.hpp> -+#include <boost/date_time/posix_time/posix_time.hpp> -+#include <gtest/gtest.h> -+ -+#include <string> -+#include <arpa/inet.h> -+#include <netinet/in.h> -+#include <sys/types.h> -+#include <sys/socket.h> -+#include <algorithm> -+#include <cstdlib> -+#include <cstddef> -+#include <list> -+#include <vector> -+#include <unistd.h> -+ -+#include <netinet/ip.h> -+#include <netinet/ip_icmp.h> -+ -+using namespace isc; -+using namespace boost::asio; -+using namespace boost::posix_time; -+using namespace isc::asiolink; -+using namespace isc::ping_check; -+using namespace isc::util; -+using namespace std; -+ -+namespace ph = std::placeholders; -+ -+namespace { -+ -+/// @brief Test timeout (ms). -+const long TEST_TIMEOUT = 10000; -+ -+/// @brief Type of the function implementing a callback invoked by the -+/// @c SocketCallback functor. -+typedef std::function<void(boost::system::error_code ec, size_t length)> -+ SocketCallbackFunction; -+ -+/// @brief Callback class for socket IO operations -+/// -+/// An instance of this object is passed to the asynchronous I/O functions -+/// and the operator() method is called when when an asynchronous I/O -+/// completes. The arguments to the completion callback are stored for later -+/// retrieval. -+class SocketCallback { -+public: -+ -+ /// @brief Structure that houses callback invocation data. -+ struct PrivateData { -+ PrivateData() : -+ error_code_(), length_(0), called_(false), name_("") -+ {} -+ -+ boost::system::error_code error_code_; ///< Completion error code -+ size_t length_; ///< Number of bytes transferred -+ bool called_; ///< Set true when callback called -+ std::string name_; ///< Which of the objects this is -+ }; -+ -+ /// @brief Constructor -+ /// -+ /// Constructs the object. It also creates the data member pointed to by -+ /// a shared pointer. When used as a callback object, this is copied as it -+ /// is passed into the asynchronous function. This means that there are two -+ /// objects and inspecting the one we passed in does not tell us anything. -+ /// -+ /// Therefore we use a boost::shared_ptr. When the object is copied, the -+ /// shared pointer is copied, which leaves both objects pointing to the same -+ /// data. -+ /// -+ /// @param which Which of the two callback objects this is -+ explicit SocketCallback(const std::string& which) : data_(new PrivateData()) -+ { -+ setName(which); -+ } -+ -+ /// @brief Destructor -+ /// -+ /// No code needed, destroying the shared pointer destroys the private data. -+ virtual ~SocketCallback() -+ {} -+ -+ /// @brief Clears the current values of invocation data members. -+ void clear() { -+ setCode(0); -+ setLength(0); -+ setCalled(false); -+ } -+ -+ /// @brief Callback Function -+ /// -+ /// Called when an asynchronous I/O completes, this stores the -+ /// completion error code and the number of bytes transferred. -+ /// -+ /// @param ec I/O completion error code passed to callback function. -+ /// @param length Number of bytes transferred -+ virtual void operator()(boost::system::error_code ec, size_t length = 0) { -+ data_->error_code_ = ec; -+ setLength(length); -+ setCalled(true); -+ } -+ -+ /// @brief Get I/O completion error code -+ int getCode() { -+ return (data_->error_code_.value()); -+ } -+ -+ /// @brief Set I/O completion code -+ /// -+ /// @param code New value of completion code -+ void setCode(int code) { -+ data_->error_code_ = boost::system::error_code(code, boost::system::error_code().category()); -+ } -+ -+ /// @brief Get number of bytes transferred in I/O -+ size_t getLength() const { -+ return (data_->length_); -+ } -+ -+ /// @brief Set number of bytes transferred in I/O -+ /// -+ /// @param length New value of length parameter -+ void setLength(size_t length) { -+ data_->length_ = length; -+ } -+ -+ /// @brief Get flag to say when callback was called -+ bool getCalled() const { -+ return (data_->called_); -+ } -+ -+ /// @brief Set flag to say when callback was called -+ /// -+ /// @param called New value of called parameter -+ void setCalled(bool called) { -+ data_->called_ = called; -+ } -+ -+ /// @brief Return instance of callback name -+ std::string getName() const { -+ return (data_->name_); -+ } -+ -+ /// @brief Set callback name -+ /// -+ /// @param name New value of the callback name -+ void setName(const std::string& name) { -+ data_->name_ = name; -+ } -+ -+private: -+ boost::shared_ptr<PrivateData> data_; ///< Pointer to private data -+}; -+ -+/// @brief Socket and pointer types for sending and receiving ICMP echos. -+typedef ICMPSocket<SocketCallback> PingSocket; -+typedef boost::shared_ptr<PingSocket> PingSocketPtr; -+ -+/// @brief Simple test fixture for testing ICMPSocket. -+class ICMPSocketTest : public ::testing::Test { -+public: -+ /// @brief Constructor. -+ ICMPSocketTest() -+ : io_service_(new IOService()), test_timer_(io_service_) { -+ test_timer_.setup(std::bind(&ICMPSocketTest::timeoutHandler, this, true), -+ TEST_TIMEOUT, IntervalTimer::ONE_SHOT); -+ } -+ -+ /// @brief Destructor. -+ virtual ~ICMPSocketTest() { -+ test_timer_.cancel(); -+ io_service_->stopAndPoll(); -+ } -+ -+ /// @brief Indicates if current user is not root -+ /// -+ /// @return True if neither the uid or the effective -+ /// uid is root. -+ static bool notRoot() { -+ return (getuid() != 0 && geteuid() != 0); -+ } -+ -+ /// @brief Callback function invoke upon test timeout. -+ /// -+ /// It stops the IO service and reports test timeout. -+ /// -+ /// @param fail_on_timeout Specifies if test failure should be reported. -+ void timeoutHandler(const bool fail_on_timeout) { -+ if (fail_on_timeout) { -+ ADD_FAILURE() << "Timeout occurred while running the test!"; -+ } -+ io_service_->stop(); -+ } -+ -+ /// @brief IOService instance used by thread pools. -+ IOServicePtr io_service_; -+ -+ /// @brief Asynchronous timer service to detect timeouts. -+ IntervalTimer test_timer_; -+ -+ /// @brief Returns pointer to the first byte of the input buffer. -+ /// -+ /// @throw InvalidOperation if called when the buffer is empty. -+ uint8_t* getInputBufData() { -+ if (input_buf_.empty()) { -+ isc_throw(InvalidOperation, "TcpConnection::getInputBufData() - cannot access empty buffer"); -+ } -+ -+ return (input_buf_.data()); -+ } -+ -+ /// @brief Returns input buffer size. -+ size_t getInputBufSize() const { -+ return (input_buf_.size()); -+ } -+ -+ /// @brief Set the capacity of the input buffer -+ /// -+ /// @param buf_size maximum number of bytes allowed in the buffer -+ void resizeInputBuf(size_t buf_size) { -+ input_buf_.resize(buf_size); -+ } -+ -+ /// @brief Buffer for a single socket read. -+ std::vector<uint8_t> input_buf_; -+}; -+ -+ -+// Verifies that an ICMP socket can be opened and closed. -+TEST_F(ICMPSocketTest, openClose) { -+ SKIP_IF(notRoot()); -+ -+ // For open the endpoint is only used to determine protocol, the address is irrelevant. -+ ICMPEndpoint ping_to_endpoint(IOAddress::IPV4_ZERO_ADDRESS()); -+ -+ PingSocket socket(io_service_); -+ SocketCallback socket_cb("open"); -+ -+ // Verify the socket is closed. -+ ASSERT_FALSE(socket.isOpen()); -+ -+ // Open the socket. -+ ASSERT_NO_THROW_LOG(socket.open(&ping_to_endpoint, socket_cb)); -+ -+ // Verify the socket is open. -+ ASSERT_TRUE(socket.isOpen()); -+ // Since open() is synchronous the callback should not have been invoked. -+ ASSERT_FALSE(socket_cb.getCalled()); -+ -+ // Opening an already open should be harmless. -+ ASSERT_NO_THROW_LOG(socket.open(&ping_to_endpoint, socket_cb)); -+ ASSERT_TRUE(socket.isOpen()); -+ -+ // Close the socket. -+ ASSERT_NO_THROW_LOG(socket.close()); -+ ASSERT_FALSE(socket.isOpen()); -+ -+ // Closing a closed socket should be harmless. -+ ASSERT_NO_THROW_LOG(socket.close()); -+ ASSERT_FALSE(socket.isOpen()); -+} -+ -+// Verifies that an ICMP socket can send and receive ICMP messages. -+TEST_F(ICMPSocketTest, sendReceive) { -+ SKIP_IF(notRoot()); -+ -+ PingSocket socket(io_service_); -+ -+ // For open the endpoint is only used to determine protocol, the address is irrelevant. -+ ICMPEndpoint endpoint(IOAddress::IPV4_ZERO_ADDRESS()); -+ -+ // Open the socket. -+ SocketCallback open_cb("open"); -+ ASSERT_NO_THROW_LOG(socket.open(&endpoint, open_cb)); -+ -+ // Build a ping. -+ struct icmp echo; -+ memset(&echo, 0, sizeof(echo)); -+ echo.icmp_type = ICMPMsg::ECHO_REQUEST; -+ echo.icmp_id = htons(0x1122); -+ echo.icmp_seq = htons(0x3344); -+ echo.icmp_cksum = htons(~(socket.calcChecksum((const uint8_t*)&echo, sizeof(echo)))); -+ -+ // Send it to the loopback. -+ IOAddress ping_to_addr("127.0.0.1"); -+ SocketCallback send_cb("send"); -+ ICMPEndpoint ping_to_endpoint(ping_to_addr); -+ ASSERT_NO_THROW_LOG(socket.asyncSend(&echo, sizeof(echo), &ping_to_endpoint, send_cb)); -+ -+ // Run the send handler. -+ io_service_->runOne(); -+ -+ // Callback should have been invoked without an error code. -+ ASSERT_TRUE(send_cb.getCalled()); -+ ASSERT_EQ(0, send_cb.getCode()); -+ // Verify we sent the whole message. -+ ASSERT_EQ(send_cb.getLength(), sizeof(echo)); -+ -+ // Call asyncReceive until we get our reply. -+ resizeInputBuf(1500); -+ ICMPEndpoint reply_endpoint; -+ SocketCallback receive_cb("receive"); -+ -+ // We need two receives when pinging loop back, only one with a real address. -+ size_t pass = 0; -+ do { -+ receive_cb.clear(); -+ memset(getInputBufData(), 0x00, getInputBufSize()); -+ ASSERT_NO_THROW(socket.asyncReceive(static_cast<void*>(getInputBufData()), -+ getInputBufSize(), 0, &reply_endpoint, receive_cb)); -+ -+ // Run the read handler. -+ io_service_->runOne(); -+ } while (++pass < 2 && (!receive_cb.getCalled())); -+ -+ // Callback should have been invoked without an error code. -+ ASSERT_TRUE(receive_cb.getCalled()); -+ ASSERT_EQ(0, receive_cb.getCode()); -+ -+ // Verify the reply came from the target address. -+ EXPECT_EQ(ping_to_addr.toText(), reply_endpoint.getAddress().toText()); -+ -+ // Verify we got at least enough data for an IP header. -+ size_t bytes_received = receive_cb.getLength(); -+ ASSERT_GE(bytes_received, sizeof(struct ip)); -+ -+ // Build the reply from data -+ uint8_t* icbuf = getInputBufData(); -+ -+ // Find the IP header length... -+ struct ip* ip_header = (struct ip*)(icbuf); -+ auto hlen = (ip_header->ip_hl << 2); -+ -+ // Make sure we received enough data. -+ ASSERT_TRUE(bytes_received >= (hlen + sizeof(struct icmp))) -+ << "received packet too short to be ICMP"; -+ -+ // Verify the message type. -+ struct icmp* reply = (struct icmp*)(icbuf + hlen); -+ auto msg_type = reply->icmp_type; -+ ASSERT_EQ(ICMPMsg::ECHO_REPLY, msg_type); -+ -+ // Verify the id and sequence values. -+ auto id = ntohs(reply->icmp_hun.ih_idseq.icd_id); -+ EXPECT_EQ(0x1122, id); -+ -+ auto sequence = ntohs(reply->icmp_hun.ih_idseq.icd_seq); -+ EXPECT_EQ(0x3344, sequence); -+ -+ // Close the socket. -+ ASSERT_NO_THROW_LOG(socket.close()); -+ ASSERT_FALSE(socket.isOpen()); -+} -+ -+} -diff --git a/src/hooks/dhcp/ping_check/tests/meson.build b/src/hooks/dhcp/ping_check/tests/meson.build -new file mode 100644 -index 0000000000..8beca7813e ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/tests/meson.build -@@ -0,0 +1,21 @@ -+if not TESTS_OPT.enabled() -+ subdir_done() -+endif -+ -+dhcp_ping_check_tests = executable( -+ 'dhcp-ping-check-tests', -+ 'config_cache_unittests.cc', -+ 'icmp_endpoint_unittests.cc', -+ 'icmp_msg_unittests.cc', -+ 'icmp_socket_unittests.cc', -+ 'ping_channel_unittests.cc', -+ 'ping_check_config_unittests.cc', -+ 'ping_check_mgr_unittests.cc', -+ 'ping_context_store_unittests.cc', -+ 'ping_context_unittests.cc', -+ 'run_unittests.cc', -+ dependencies: [CRYPTO_DEP, GTEST_DEP], -+ include_directories: [include_directories('.'), include_directories('..')] + INCLUDES, -+ link_with: [dhcp_ping_check_archive] + LIBS_BUILT_SO_FAR, -+) -+test('dhcp-ping-check-tests', dhcp_ping_check_tests, protocol: 'gtest') -diff --git a/src/hooks/dhcp/ping_check/tests/ping_channel_unittests.cc b/src/hooks/dhcp/ping_check/tests/ping_channel_unittests.cc -new file mode 100644 -index 0000000000..4c57a2e500 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/tests/ping_channel_unittests.cc -@@ -0,0 +1,821 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+/// @file This file contains tests which exercise the PingChannel class. -+ -+#include <config.h> -+ -+#include <ping_channel.h> -+#include <ping_test_utils.h> -+#include <asiolink/interval_timer.h> -+#include <asiolink/io_service_thread_pool.h> -+#include <dhcp/iface_mgr.h> -+#include <util/multi_threading_mgr.h> -+#include <testutils/multi_threading_utils.h> -+#include <testutils/gtest_utils.h> -+#include <gtest/gtest.h> -+ -+#include <boost/multi_index/indexed_by.hpp> -+#include <boost/multi_index/member.hpp> -+#include <boost/multi_index/mem_fun.hpp> -+#include <boost/multi_index/hashed_index.hpp> -+#include <boost/multi_index/ordered_index.hpp> -+#include <boost/multi_index_container.hpp> -+#include <boost/multi_index/composite_key.hpp> -+ -+#include <queue> -+#include <list> -+#include <thread> -+#include <mutex> -+ -+using namespace std; -+using namespace isc; -+using namespace isc::asiolink; -+using namespace isc::dhcp; -+using namespace isc::ping_check; -+using namespace isc::util; -+using namespace isc::test; -+using namespace boost::asio::error; -+ -+namespace ph = std::placeholders; -+ -+namespace { -+ -+/// @brief Tag for index by address. -+struct AddressIdSequenceIndexTag { }; -+ -+/// @brief A multi index container holding pointers ICMPMsgPtr -+/// -+/// The message may be accessed using the following index(es): -+/// - using an IPv4 address, id, and sequence number -+typedef boost::multi_index_container< -+ // It holds pointers to ICMPMsg objects. -+ ICMPMsgPtr, -+ boost::multi_index::indexed_by< -+ // Specification of the first index starts here. -+ // This index sorts PingContexts by IPv4 addresses represented as -+ // IOAddress objects. -+ // Specification of the first index starts here. -+ boost::multi_index::ordered_unique< -+ boost::multi_index::tag<AddressIdSequenceIndexTag>, -+ boost::multi_index::composite_key< -+ ICMPMsg, -+ // The boolean value specifying if context is waiting for a reply -+ boost::multi_index::const_mem_fun<ICMPMsg, const IOAddress&, -+ &ICMPMsg::getSource>, -+ boost::multi_index::const_mem_fun<ICMPMsg, uint16_t, -+ &ICMPMsg::getId>, -+ boost::multi_index::const_mem_fun<ICMPMsg, uint16_t, -+ &ICMPMsg::getSequence> -+ > -+ > -+ > -+> ReplyContainer; -+ -+/// @brief Single-threaded test fixture for exercising a PingChannel. -+class PingChannelTest : public IOServiceTest { -+public: -+ /// @brief Constructor -+ PingChannelTest() : mutex_(new mutex()), stopped_(false) { -+ MultiThreadingMgr::instance().setMode(false); -+ }; -+ -+ /// @brief Destructor -+ virtual ~PingChannelTest() { -+ stopped_ = true; -+ if (channel_) { -+ channel_->close(); -+ } -+ if (ios_pool_) { -+ ios_pool_->getIOService()->stopAndPoll(); -+ ios_pool_->stop(); -+ } -+ ios_pool_.reset(); -+ test_timer_.cancel(); -+ test_io_service_->stopAndPoll(); -+ MultiThreadingMgr::instance().setMode(false); -+ } -+ -+ /// @brief Called prior to test destruction. -+ /// Ensure we stop the pool in the even a test failed in an unexpected -+ /// manner that left it running. Otherwise we can get false TSAN complaints. -+ virtual void TearDown() { -+ // Stop the thread pool (if one). -+ if (ios_pool_) { -+ ios_pool_->stop(); -+ } -+ } -+ -+ /// @brief Initializes the IOServiceThreadPool -+ /// -+ /// @param num_threads number of threads in the pool -+ /// @param defer_start enables deferred start of the pool's IOService -+ void initThreadPool(size_t num_threads = 1, bool defer_start = false) { -+ ios_pool_.reset(new IoServiceThreadPool(IOServicePtr(), num_threads, defer_start)); -+ }; -+ -+ /// @brief Callback to invoke to fetch the next ping target. -+ /// -+ /// Fetches the next entry from the front of the send queue (if one). Checks for -+ /// test completion before returning. -+ /// -+ /// @param[out] next upon return it will contain the next target address. Contents are -+ /// only meaningful if the function returns true. -+ /// -+ /// @return True another target address exists, false otherwise. -+ virtual bool nextToSend(IOAddress& next) { -+ if (stopped_) { -+ return (false); -+ } -+ MultiThreadingLock lock(*mutex_); -+ bool use_next = true; -+ if (send_queue_.empty()) { -+ use_next = false; -+ } else { -+ next = send_queue_.front(); -+ } -+ -+ stopIfDone(); -+ return (use_next); -+ } -+ -+ /// @brief Callback to invoke when an ECHO write has completed. -+ /// -+ /// Ensures the completed echo matches the front of the send queue and then -+ /// pops it from the front of the queue. Checks for test completion before -+ /// returning. -+ /// -+ /// @param echo ICMP echo message that as sent -+ virtual void echoSent(ICMPMsgPtr& echo, bool send_failed) { -+ if (stopped_) { -+ return; -+ } -+ MultiThreadingLock lock(*mutex_); -+ ASSERT_EQ(echo->getDestination(), send_queue_.front()) << "send queue mismatch"; -+ send_queue_.pop(); -+ if (!send_failed) { -+ echos_sent_.push_back(echo); -+ } -+ stopIfDone(); -+ } -+ -+ /// @brief Callback to invoke when an ICMP reply has been received. -+ /// -+ /// Stores the reply if it is an ECHO REPLY message. We check to the -+ /// do avoid storing our outbound ECHO REQUESTs when testing with loop back -+ /// address. Checks for test completion before returning. -+ /// -+ /// @param reply ICMP message that was received -+ virtual void replyReceived(ICMPMsgPtr& reply) { -+ if (stopped_) { -+ return; -+ } -+ MultiThreadingLock lock(*mutex_); -+ if (reply->getType() == ICMPMsg::ECHO_REPLY) { -+ // If loopback routing is enabled, Insert the original destination address -+ // as the reply's source address. -+ if (channel_->route_loopback_) { -+ IOAddress address = channel_->loopback_map_.find(reply->getSequence()); -+ if (address != IOAddress::IPV4_ZERO_ADDRESS()) { -+ reply->setSource(address); -+ } -+ } -+ -+ replies_received_.push_back(reply); -+ storeReply(reply); -+ } -+ -+ stopIfDone(); -+ } -+ -+ /// @brief Tests that a channel can send and receive, reliably -+ /// in either single or multi-threaded mode. -+ /// -+ /// The test queues the given number of requests, beginning with -+ /// address 127.0.0.1 and incrementing the address through the number -+ /// of targets. It then opens the channel and initiates reading and -+ /// and writing, running until the test completes or times out. -+ /// It expects to receive a reply for every request. -+ /// -+ /// @param num_threads number of threads in the thread pool. If 0, -+ /// the channel will be single-threaded, sharing the test's IOService, -+ /// otherwise the channel will be driven by an IOServiceThreadPool with -+ /// the given number of threads. -+ /// @param num_targets number of target IP addresses to ping. Must not -+ /// be greater than 253. -+ /// @param set_error_trigger optional function that sets the error trigger -+ /// condition. -+ void sendReceiveTest(size_t num_threads, size_t num_targets = 25, -+ const std::function<void()>& set_error_trigger = [](){}); -+ -+ /// @brief Tests for graceful behavior when a channel encounters a read -+ /// or write error, in either single or multi-threaded mode. -+ /// -+ /// The test runs in two passes. The first pass sends and receives until -+ /// the error trigger occurs. The error should induce a graceful cessation -+ /// of operations. After verifying expected state of affairs, the second pass -+ /// is begun by re-opening the channel and resuming operations until the test -+ /// completes or times out. -+ /// -+ /// @param set_error_trigger function that sets the error trigger condition -+ /// @param num_threads number of threads in the thread pool. If 0, -+ /// the channel will be single-threaded, sharing the test's IOService, -+ /// otherwise the channel will be driven by an IOServiceThreadPool with -+ /// the given number of threads. -+ /// @param num_targets number of target IP addresses to ping. Must not -+ /// be greater than 253. -+ void ioErrorTest(const std::function<void()>& set_error_trigger, -+ size_t num_threads, size_t num_targets = 10); -+ -+ /// @brief Adds a reply to reply store. -+ /// -+ /// Fails if a reply for the same address, id, and sequence number is already -+ /// in the store. Must be used in a thread-safe context. -+ /// -+ /// @param reply reply to store -+ void storeReply(ICMPMsgPtr& reply) { -+ auto retpair = replies_map_.insert(reply); -+ ASSERT_TRUE(retpair.second) -+ << "failed to insert reply for: " << reply->getSource() -+ << ", id: " << reply->getId() << ", sequence: " << reply->getSequence(); -+ } -+ -+ /// @brief Fetches a reply from the store that matches a given ECHO -+ /// -+ /// Must be used in a thread-safe context. -+ /// -+ /// @param echo echo for whom a reply is sought -+ /// -+ /// @return The matching reply if found, otherwise an empty ICMPMsgPtr. -+ ICMPMsgPtr findReply(const ICMPMsgPtr& echo) { -+ auto const& index = replies_map_.get<AddressIdSequenceIndexTag>(); -+ auto key = boost::make_tuple(echo->getDestination(), echo->getId(), echo->getSequence()); -+ auto iter = index.find(key); -+ return (iter == index.end() ? ICMPMsgPtr() : *iter); -+ } -+ -+ /// @brief Channel instance. -+ TestablePingChannelPtr channel_; -+ -+ /// @brief IoServiceThreadPool instance -+ IoServiceThreadPoolPtr ios_pool_; -+ -+ /// @brief The mutex used to protect internal state. -+ const boost::scoped_ptr<std::mutex> mutex_; -+ -+ /// @brief Queue of IOAddresses for which to send ECHO REQUESTs. -+ std::queue<IOAddress> send_queue_; -+ -+ /// @brief List of ECHO REQUESTs that have been successfully sent in the order -+ /// they were sent. -+ std::list<ICMPMsgPtr> echos_sent_; -+ -+ /// @brief List of ECHO REPLYs that have been successfully received in the -+ /// order they were received. -+ std::list<ICMPMsgPtr> replies_received_; -+ -+ /// @brief Map of ECHO REPLYs received, indexed by source IP, id, and sequence number. -+ ReplyContainer replies_map_; -+ -+ /// @brief Flag which indicates that the manager has been stopped. -+ bool stopped_; -+}; -+ -+void -+PingChannelTest::sendReceiveTest(size_t num_threads, size_t num_targets /* = 25 */, -+ const std::function<void()>& set_error_trigger) { -+ stopped_ = false; -+ -+ // Clear state. -+ send_queue_ = {}; -+ echos_sent_.clear(); -+ replies_received_.clear(); -+ replies_map_.clear(); -+ -+ SKIP_IF(notRoot()); -+ -+ ASSERT_TRUE(num_targets < 253); -+ auto channel_ios = test_io_service_; -+ if (num_threads) { -+ // Enable MT mode. -+ util::MultiThreadingMgr::instance().setMode(true); -+ -+ // Initialize the thread pool to num_threads, defer start. -+ ASSERT_NO_THROW_LOG(initThreadPool(num_threads, true)); -+ ASSERT_TRUE(ios_pool_->isStopped()); -+ channel_ios = ios_pool_->getIOService(); -+ } -+ -+ // Create the channel instance with the appropriate io_service. -+ ASSERT_NO_THROW_LOG(channel_.reset(new TestablePingChannel( -+ channel_ios, -+ std::bind(&PingChannelTest::nextToSend, this, ph::_1), -+ std::bind(&PingChannelTest::echoSent, this, ph::_1, ph::_2), -+ std::bind(&PingChannelTest::replyReceived, this, ph::_1) -+ ))); -+ -+ // Create the callback to check test completion criteria. -+ // It returns true if we have sent out all the echos and received -+ // all the replies. -+ test_done_cb_ = [this]() { -+ return (send_queue_.empty() && (echos_sent_.size() == replies_received_.size())); -+ }; -+ -+ // Fill the send queue with num_target addresses to ping. -+ IOAddress target("127.0.0.1"); -+ for (auto i = 0; i < num_targets; ++i) { -+ send_queue_.push(target); -+ target = IOAddress::increase(target); -+ } -+ -+ (set_error_trigger)(); -+ -+ // Open the channel. -+ ASSERT_NO_THROW_LOG(channel_->open()); -+ ASSERT_TRUE(channel_->isOpen()); -+ -+ if (num_threads) { -+ ios_pool_->run(); -+ } -+ -+ // Initiate reading and writing. -+ ASSERT_NO_THROW_LOG(channel_->startRead()); -+ ASSERT_NO_THROW_LOG(channel_->startSend()); -+ -+ // Run the main thread's IOService until we complete or timeout. -+ ASSERT_NO_THROW_LOG(runIOService(1000)); -+ -+ if (ios_pool_) { -+ // Stop the thread pool. -+ ASSERT_NO_THROW_LOG(ios_pool_->stop()); -+ ASSERT_TRUE(ios_pool_->isStopped()); -+ } -+ -+ // Send queue should be empty. -+ EXPECT_TRUE(send_queue_.empty()); -+ -+ // Should have as many replies as echos. -+ EXPECT_EQ(echos_sent_.size(), replies_received_.size()); -+ -+ // Should have a reply for every echo. -+ for (auto const& echo : echos_sent_) { -+ ICMPMsgPtr reply = findReply(echo); -+ EXPECT_TRUE(reply) << "no reply found for:" << echo->getDestination() -+ << ", id:" << echo->getId() << ", sequence: " << echo->getSequence(); -+ } -+ -+ stopped_ = true; -+ if (channel_) { -+ channel_->close(); -+ } -+ if (ios_pool_) { -+ ios_pool_->getIOService()->stopAndPoll(); -+ ios_pool_->stop(); -+ } -+ ios_pool_.reset(); -+ test_timer_.cancel(); -+ test_io_service_->stopAndPoll(); -+ MultiThreadingMgr::instance().setMode(false); -+} -+ -+void -+PingChannelTest::ioErrorTest(const std::function<void()>& set_error_trigger, -+ size_t num_threads, size_t num_targets) { -+ ASSERT_TRUE(num_targets < 253); -+ SKIP_IF(notRoot()); -+ -+ ASSERT_TRUE(replies_received_.empty()); -+ -+ /// If it's an MT test create the thread pool. -+ auto channel_ios = test_io_service_; -+ if (num_threads) { -+ // Enable MT mode. -+ util::MultiThreadingMgr::instance().setMode(true); -+ -+ // Initialize the thread pool to num_threads, defer start. -+ ASSERT_NO_THROW_LOG(initThreadPool(num_threads, true)); -+ ASSERT_TRUE(ios_pool_->isStopped()); -+ channel_ios = ios_pool_->getIOService(); -+ } -+ -+ // Set local shutdown called flag to false. -+ bool shutdown_cb_called = false; -+ -+ // Create the channel instance with the appropriate io_service. -+ ASSERT_NO_THROW_LOG(channel_.reset(new TestablePingChannel( -+ channel_ios, -+ std::bind(&PingChannelTest::nextToSend, this, ph::_1), -+ std::bind(&PingChannelTest::echoSent, this, ph::_1, ph::_2), -+ std::bind(&PingChannelTest::replyReceived, this, ph::_1), -+ ([this, &shutdown_cb_called]() { -+ shutdown_cb_called = true; -+ test_io_service_->stop(); -+ }) -+ ))); -+ -+ // Set the test_done_cb_ to always return false (i.e. test is not -+ // done). -+ test_done_cb_ = []() { -+ return (false); -+ }; -+ -+ // Fill the send queue with target addresses to ping. -+ IOAddress target("127.0.0.1"); -+ for (auto i = 0; i < (num_targets / 2); ++i) { -+ send_queue_.push(target); -+ target = IOAddress::increase(target); -+ } -+ -+ // Set the error trigger. -+ (set_error_trigger)(); -+ -+ // FIRST PASS -+ -+ // Open the channel. -+ ASSERT_NO_THROW_LOG(channel_->open()); -+ ASSERT_TRUE(channel_->isOpen()); -+ -+ if (num_threads) { -+ ios_pool_->run(); -+ } -+ -+ // Initiate reading and writing. -+ ASSERT_NO_THROW_LOG(channel_->startRead()); -+ ASSERT_NO_THROW_LOG(channel_->startSend()); -+ -+ // Run the main thread's IOService until we stop or timeout. -+ ASSERT_NO_THROW_LOG(runIOService(1000)); -+ -+ // Shutdown callback should have been invoked, the channel should be closed, -+ // but the pool should still be running. -+ ASSERT_TRUE(shutdown_cb_called); -+ ASSERT_FALSE(channel_->isOpen()); -+ -+ if (ios_pool_) { -+ ASSERT_TRUE(ios_pool_->isRunning()); -+ -+ // Pause the thread pool. -+ ASSERT_NO_THROW_LOG(ios_pool_->pause()); -+ ASSERT_TRUE(ios_pool_->isPaused()); -+ } -+ -+ // Save how many echos sent and replies received during the first pass. -+ auto first_pass_echo_count = echos_sent_.size(); -+ auto first_pass_reply_count = replies_received_.size(); -+ -+ // Should have sent some but not all. -+ EXPECT_LE(first_pass_echo_count, num_targets); -+ -+ // SECOND PASS -+ -+ // Modify the test done callback to check test completion criteria. -+ // It returns true if we have sent out all the echos and received -+ // all the replies. -+ test_done_cb_ = [this, &first_pass_reply_count]() { -+ return (send_queue_.empty() && (replies_received_.size() > first_pass_reply_count)); -+ }; -+ -+ // Fill the send queue with target addresses to ping. -+ for (auto i = 0; i < (num_targets / 2); ++i) { -+ send_queue_.push(target); -+ target = IOAddress::increase(target); -+ } -+ -+ // Resume running the thread pool (if one). -+ if (ios_pool_) { -+ ASSERT_NO_THROW_LOG(ios_pool_->run()); -+ ASSERT_TRUE(ios_pool_->isRunning()); -+ } -+ -+ // Resume reopening the channel and restarting IO operations. -+ ASSERT_NO_THROW_LOG(channel_->open()); -+ ASSERT_TRUE(channel_->isOpen()); -+ ASSERT_NO_THROW_LOG(channel_->startRead()); -+ ASSERT_NO_THROW_LOG(channel_->startSend()); -+ -+ // Run the main thread's IOService until we complete or timeout. -+ ASSERT_NO_THROW_LOG(runIOService(1000)); -+ -+ // Stop the thread pool (if one). -+ if (ios_pool_) { -+ ASSERT_NO_THROW_LOG(ios_pool_->stop()); -+ ASSERT_TRUE(ios_pool_->isStopped()); -+ } -+ -+ // Send queue should be empty. -+ EXPECT_TRUE(send_queue_.empty()); -+ -+ // Should have sent as many echos as we queued. -+ EXPECT_EQ(echos_sent_.size(), num_targets); -+ -+ // Should have more replies than we had, but likely not all. -+ EXPECT_GE(replies_received_.size(), first_pass_reply_count); -+} -+ -+// Verifies PingChannel open and close operations. -+TEST_F(PingChannelTest, openCloseST) { -+ SKIP_IF(notRoot()); -+ -+ // Create the channel instance. -+ ASSERT_NO_THROW_LOG(channel_.reset(new TestablePingChannel( -+ test_io_service_, -+ std::bind(&PingChannelTest::nextToSend, this, ph::_1), -+ std::bind(&PingChannelTest::echoSent, this, ph::_1, ph::_2), -+ std::bind(&PingChannelTest::replyReceived, this, ph::_1) -+ ))); -+ -+ ASSERT_TRUE(channel_); -+ -+ ASSERT_TRUE(channel_->getSingleThreaded()); -+ -+ // Verify it is not open. -+ ASSERT_FALSE(channel_->isOpen()); -+ -+ EXPECT_FALSE(channel_->getWatchSocket()); -+ EXPECT_EQ(channel_->getRegisteredWriteFd(), -1); -+ EXPECT_EQ(channel_->getRegisteredReadFd(), -1); -+ -+ // Verify that invoking close is harmless. -+ ASSERT_NO_THROW_LOG(channel_->close()); -+ -+ // Attempt to open the channel. -+ ASSERT_NO_THROW_LOG(channel_->open()); -+ -+ // PingChannel::open() is synchronous and while it has a callback -+ // it should never be invoked. Run the service to make sure. -+ ASSERT_NO_THROW_LOG(runIOService(1000)); -+ -+ // Verify the channel is open. -+ ASSERT_TRUE(channel_->isOpen()); -+ -+ // Verify the WatchSocket was created and that its fd and that of the -+ // PingSocket are both registered with IfaceMgr. -+ ASSERT_TRUE(channel_->getWatchSocket()); -+ int registered_write_fd = channel_->getRegisteredWriteFd(); -+ EXPECT_EQ(registered_write_fd, channel_->getWatchSocket()->getSelectFd()); -+ EXPECT_TRUE(IfaceMgr::instance().isExternalSocket(registered_write_fd)); -+ int registered_read_fd = channel_->getRegisteredReadFd(); -+ EXPECT_EQ(registered_read_fd, channel_->getPingSocket()->getNative()); -+ EXPECT_TRUE(IfaceMgr::instance().isExternalSocket(registered_read_fd)); -+ -+ // A subsequent open should be harmless. -+ ASSERT_NO_THROW_LOG(channel_->open()); -+ -+ // Closing the socket should work. -+ ASSERT_NO_THROW_LOG(channel_->close()); -+ -+ // Verify watch socket is gone, registered fds are reset, and prior -+ // registered fds are no longer registered. -+ EXPECT_FALSE(channel_->getWatchSocket()); -+ EXPECT_EQ(channel_->getRegisteredWriteFd(), -1); -+ EXPECT_FALSE(IfaceMgr::instance().isExternalSocket(registered_write_fd)); -+ EXPECT_EQ(channel_->getRegisteredReadFd(), -1); -+ EXPECT_FALSE(IfaceMgr::instance().isExternalSocket(registered_read_fd)); -+ -+ // Verify it is not open. -+ ASSERT_FALSE(channel_->isOpen()); -+} -+ -+// Verifies PingChannel open and close operations. -+TEST_F(PingChannelTest, openCloseMT) { -+ SKIP_IF(notRoot()); -+ MultiThreadingTest mt; -+ -+ // Create the channel instance. -+ ASSERT_NO_THROW_LOG(channel_.reset(new TestablePingChannel( -+ test_io_service_, -+ std::bind(&PingChannelTest::nextToSend, this, ph::_1), -+ std::bind(&PingChannelTest::echoSent, this, ph::_1, ph::_2), -+ std::bind(&PingChannelTest::replyReceived, this, ph::_1) -+ ))); -+ -+ ASSERT_TRUE(channel_); -+ -+ ASSERT_FALSE(channel_->getSingleThreaded()); -+ -+ // Verify it is not open. -+ ASSERT_FALSE(channel_->isOpen()); -+ -+ // Verify that invoking close is harmless. -+ ASSERT_NO_THROW_LOG(channel_->close()); -+ -+ // Attempt to open the channel. -+ ASSERT_NO_THROW_LOG(channel_->open()); -+ -+ // PingChannel::open() is synchronous and while it has a callback -+ // it should never be invoked. Run the service to make sure. -+ ASSERT_NO_THROW_LOG(runIOService(1000)); -+ -+ // Verify the channel is open. -+ ASSERT_TRUE(channel_->isOpen()); -+ -+ // Verify that single-threaded members are not set. -+ EXPECT_FALSE(channel_->getWatchSocket()); -+ EXPECT_EQ(channel_->getRegisteredWriteFd(), -1); -+ EXPECT_EQ(channel_->getRegisteredReadFd(), -1); -+ -+ // A subsequent open should be harmless. -+ ASSERT_NO_THROW_LOG(channel_->open()); -+ -+ // Closing the socket should work. -+ ASSERT_NO_THROW_LOG(channel_->close()); -+ -+ // Verify it is not open. -+ ASSERT_FALSE(channel_->isOpen()); -+} -+ -+// Verifies that a PingChannel can perpetuate sending requests and receiving -+// replies when driven by a single-threaded IOService. -+TEST_F(PingChannelTest, sendReceiveST) { -+ sendReceiveTest(0); -+} -+ -+// Verifies that a PingChannel can perpetuate sending requests and receiving -+// replies when driven by a multi-threaded IOServiceThreadPool 3 threads -+TEST_F(PingChannelTest, sendReceiveMT) { -+ // Use a thread pool with 3 threads. -+ sendReceiveTest(3); -+} -+ -+// Verifies that an exception throw from asyncRead triggers graceful channel -+// shutdown and that operations can be resumed with a single-threaded channel. -+TEST_F(PingChannelTest, readExceptionErrorST) { -+ ioErrorTest( -+ [this]() { -+ channel_->throw_on_read_number_ = 5; -+ }, 0); -+} -+ -+// Verifies that an exception throw from asyncRead triggers graceful channel -+// shutdown and that operations can be resumed with a multi-threaded channel. -+TEST_F(PingChannelTest, readExceptionErrorMT) { -+ // Use a thread pool with 3 threads. -+ ioErrorTest( -+ [this]() { -+ channel_->throw_on_read_number_ = 5; -+ }, 3, 20); -+} -+ -+// Verifies that a fatal error code passed into socketReadCallback triggers graceful channel -+// shutdown and that operations can be resumed with a single-threaded channel. -+TEST_F(PingChannelTest, readFatalErrorST) { -+ ioErrorTest( -+ [this]() { -+ channel_->ec_on_read_number_ = 3; -+ // See boost/asio/error.hpp for error codes -+ channel_->read_error_ec_ = make_error_code(fault); -+ }, 0); -+} -+ -+// Verifies that a fatal error code passed into socketReadCallback triggers graceful channel -+// shutdown and that operations can be resumed with a single-threaded channel. -+TEST_F(PingChannelTest, readFatalErrorMT) { -+ ioErrorTest( -+ [this]() { -+ channel_->ec_on_read_number_ = 3; -+ // See boost/asio/error.hpp for error codes -+ channel_->read_error_ec_ = make_error_code(fault); -+ }, 4); -+} -+ -+// Verifies that a non-fatal, EWOULDBLOCK error passed into socketReadCallback does -+// not disrupt reading for a single-threaded channel. -+TEST_F(PingChannelTest, readAgainErrorST) { -+ sendReceiveTest(0, 10, -+ [this]() { -+ channel_->ec_on_read_number_ = 4; -+ // See boost/asio/error.hpp for error codes -+ channel_->read_error_ec_ = make_error_code(would_block); -+ }); -+} -+ -+// Verifies that a non-fatal, EWOULDBLOCK error passed into socketReadCallback does -+// not disrupt reading for a multi-threaded channel. -+TEST_F(PingChannelTest, readAgainErrorMT) { -+ sendReceiveTest(3, 10, -+ [this]() { -+ channel_->ec_on_read_number_ = 4; -+ // See boost/asio/error.hpp for error codes -+ channel_->read_error_ec_ = make_error_code(would_block); -+ }); -+} -+ -+// Verifies that an exception throw from asyncRead triggers graceful channel -+// shutdown and that operations can be resumed with a single-threaded channel. -+TEST_F(PingChannelTest, writeExceptionErrorST) { -+ ioErrorTest( -+ [this]() { -+ channel_->throw_on_write_number_ = 5; -+ }, 0); -+} -+ -+// Verifies that an exception throw from asyncRead triggers graceful channel -+// shutdown and that operations can be resumed with a multi-threaded channel. -+TEST_F(PingChannelTest, writeExceptionErrorMT) { -+ // Use a thread pool with 3 threads. -+ ioErrorTest( -+ [this]() { -+ channel_->throw_on_write_number_ = 5; -+ }, 3); -+} -+ -+// Verifies that a fatal error code passed into socketReadCallback triggers graceful channel -+// shutdown and that operations can be resumed with a single-threaded channel. -+TEST_F(PingChannelTest, writeFatalErrorST) { -+ ioErrorTest( -+ [this]() { -+ channel_->ec_on_write_number_ = 3; -+ // See boost/asio/error.hpp for error codes -+ channel_->write_error_ec_ = make_error_code(fault); -+ }, 0); -+} -+ -+// Verifies that a fatal error code passed into socketReadCallback triggers graceful channel -+// shutdown and that operations can be resumed with a single-threaded channel. -+TEST_F(PingChannelTest, writeFatalErrorMT) { -+ ioErrorTest( -+ [this]() { -+ channel_->ec_on_write_number_ = 3; -+ // See boost/asio/error.hpp for error codes -+ channel_->write_error_ec_ = make_error_code(fault); -+ }, 4); -+} -+ -+// Verifies that a non-fatal, EWOULDBLOCK error passed into socketWriteCallback does -+// not disrupt writing for a single-threaded channel. -+TEST_F(PingChannelTest, writeAgainErrorST) { -+ sendReceiveTest(0, 10, -+ [this]() { -+ channel_->ec_on_write_number_ = 6; -+ // See boost/asio/error.hpp for error codes -+ channel_->write_error_ec_ = make_error_code(would_block); -+ }); -+} -+ -+// Verifies that a non-fatal, EWOULDBLOCK error passed into socketWriteCallback -+// does not disrupt writing for a multi-threaded channel. -+TEST_F(PingChannelTest, writeAgainErrorMT) { -+ sendReceiveTest(3, 10, -+ [this]() { -+ channel_->ec_on_write_number_ = 6; -+ // See boost/asio/error.hpp for error codes -+ channel_->write_error_ec_ = make_error_code(would_block); -+ }); -+} -+ -+// Verify the recoverable write errors do not disrupt writing for a -+// single-threaded channel. -+TEST_F(PingChannelTest, writeSendFailedErrorST) { -+ SKIP_IF(notRoot()); -+ -+ std::list<boost::asio::error::basic_errors> errors = { -+ boost::asio::error::network_unreachable, -+ boost::asio::error::host_unreachable, -+ boost::asio::error::network_down, -+ boost::asio::error::no_buffer_space, -+ boost::asio::error::access_denied -+ }; -+ -+ for (auto const& error : errors) { -+ sendReceiveTest(0, 10, -+ [this, error]() { -+ channel_->ec_on_write_number_ = 6; -+ // See boost/asio/error.hpp for error codes -+ channel_->write_error_ec_ = make_error_code(error); -+ }); -+ -+ // Sanity check, we should have sent one less than we targeted. -+ EXPECT_EQ(echos_sent_.size(), 9); -+ } -+} -+ -+// Verify the recoverable write errors do not disrupt writing for a -+// multi-threaded channel. -+TEST_F(PingChannelTest, writeSendFailedErrorMT) { -+ SKIP_IF(notRoot()); -+ -+ std::list<boost::asio::error::basic_errors> errors = { -+ boost::asio::error::network_unreachable, -+ boost::asio::error::host_unreachable, -+ boost::asio::error::network_down, -+ boost::asio::error::no_buffer_space, -+ boost::asio::error::access_denied -+ }; -+ -+ for (auto const& error : errors) { -+ sendReceiveTest(3, 10, -+ [this, error]() { -+ channel_->ec_on_write_number_ = 6; -+ // See boost/asio/error.hpp for error codes -+ channel_->write_error_ec_ = make_error_code(error); -+ }); -+ -+ // Sanity check, we should have sent one less than we targeted. -+ EXPECT_EQ(echos_sent_.size(), 9); -+ } -+} -+ -+} // end of anonymous namespace -diff --git a/src/hooks/dhcp/ping_check/tests/ping_check_config_unittests.cc b/src/hooks/dhcp/ping_check/tests/ping_check_config_unittests.cc -new file mode 100644 -index 0000000000..a831a0efab ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/tests/ping_check_config_unittests.cc -@@ -0,0 +1,287 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+/// @file This file contains tests which exercise the PingCheckConfig class. -+ -+#include <config.h> -+#include <ping_check_config.h> -+#include <testutils/gtest_utils.h> -+ -+#include <gtest/gtest.h> -+#include <list> -+ -+using namespace std; -+using namespace isc; -+using namespace isc::data; -+using namespace isc::ping_check; -+ -+namespace { -+ -+// Verifies PingCheckConfig constructors and accessors. -+TEST(PingCheckConfigTest, basics) { -+ PingCheckConfig config; -+ -+ // Verify initial values. -+ EXPECT_TRUE(config.getEnablePingCheck()); -+ EXPECT_EQ(1, config.getMinPingRequests()); -+ EXPECT_EQ(100, config.getReplyTimeout()); -+ EXPECT_EQ(60, config.getPingClttSecs()); -+ EXPECT_EQ(0, config.getPingChannelThreads()); -+ -+ // Verify accessors. -+ EXPECT_NO_THROW_LOG(config.setEnablePingCheck(false)); -+ EXPECT_FALSE(config.getEnablePingCheck()); -+ -+ EXPECT_NO_THROW_LOG(config.setMinPingRequests(4)); -+ EXPECT_EQ(4, config.getMinPingRequests()); -+ -+ EXPECT_NO_THROW_LOG(config.setReplyTimeout(250)); -+ EXPECT_EQ(250, config.getReplyTimeout()); -+ -+ EXPECT_NO_THROW_LOG(config.setPingClttSecs(120)); -+ EXPECT_EQ(120, config.getPingClttSecs()); -+ -+ EXPECT_NO_THROW_LOG(config.setPingChannelThreads(6)); -+ EXPECT_EQ(6, config.getPingChannelThreads()); -+ -+ // Verify copy construction. -+ PingCheckConfig config2(config); -+ EXPECT_FALSE(config2.getEnablePingCheck()); -+ EXPECT_EQ(4, config2.getMinPingRequests()); -+ EXPECT_EQ(250, config2.getReplyTimeout()); -+ EXPECT_EQ(120, config2.getPingClttSecs()); -+ EXPECT_EQ(6, config2.getPingChannelThreads()); -+} -+ -+// Exercises PingCheckConfig parameter parsing with valid configuration -+// permutations. -+TEST(PingCheckConfigTest, parseValidScenarios) { -+ // Describes a test scenario. -+ struct Scenario { -+ int line_; // Scenario line number -+ std::string json_; // JSON configuration to parse -+ bool exp_enable_ping_check_; // Expected value for enable-ping-check -+ uint32_t exp_min_ping_requests_; // Expected value for min-ping-requests -+ uint32_t exp_reply_timeout_; // Expected value for reply-timeout -+ uint32_t exp_ping_cltt_secs_; // Expected value for ping-cltt-secs -+ size_t exp_num_threads_; // Expected value for ping-channel-threads -+ }; -+ -+ // List of test scenarios to run. -+ list<Scenario> scenarios = { -+ { -+ // Empty map -+ __LINE__, -+ R"({ })", -+ true, 1, 100, 60, 0 -+ }, -+ { -+ // Only enable-ping-check", -+ __LINE__, -+ R"({ "enable-ping-check" : false })", -+ false, 1, 100, 60, 0 -+ }, -+ { -+ // Only min-ping-requests", -+ __LINE__, -+ R"({ "min-ping-requests" : 3 })", -+ true, 3, 100, 60, 0 -+ }, -+ { -+ // Only reply-timeout", -+ __LINE__, -+ R"({ "reply-timeout" : 250 })", -+ true, 1, 250, 60, 0 -+ }, -+ { -+ // Only ping-cltt-secs", -+ __LINE__, -+ R"({ "ping-cltt-secs" : 77 })", -+ true, 1, 100, 77, 0 -+ }, -+ { -+ // Only ping-channel-threads", -+ __LINE__, -+ R"({ "ping-channel-threads" : 5 })", -+ true, 1, 100, 60, 5 -+ }, -+ { -+ // All parameters", -+ __LINE__, -+ R"( -+ { -+ "enable-ping-check" : false, -+ "min-ping-requests" : 2, -+ "reply-timeout" : 375, -+ "ping-cltt-secs" : 120, -+ "ping-channel-threads" : 6 -+ })", -+ false, 2, 375, 120, 6 -+ }, -+ }; -+ -+ // Iterate over the scenarios. -+ for (auto const& scenario : scenarios) { -+ stringstream oss; -+ oss << "scenario at line: " << scenario.line_; -+ SCOPED_TRACE(oss.str()); -+ -+ // Convert JSON texts to Element map. -+ ConstElementPtr json_elements; -+ ASSERT_NO_THROW_LOG(json_elements = Element::fromJSON(scenario.json_)); -+ -+ // Parsing elements should succeed. -+ PingCheckConfig config; -+ ASSERT_NO_THROW_LOG(config.parse(json_elements)); -+ -+ // Verify expected values. -+ EXPECT_EQ(scenario.exp_enable_ping_check_, config.getEnablePingCheck()); -+ EXPECT_EQ(scenario.exp_min_ping_requests_, config.getMinPingRequests()); -+ EXPECT_EQ(scenario.exp_reply_timeout_, config.getReplyTimeout()); -+ EXPECT_EQ(scenario.exp_ping_cltt_secs_, config.getPingClttSecs()); -+ EXPECT_EQ(scenario.exp_num_threads_, config.getPingChannelThreads()); -+ } -+} -+ -+// Exercises PingCheckConfig parameter parsing with invalid configuration -+// permutations. -+TEST(PingCheckConfigTest, parseInvalidScenarios) { -+ // Describes a test scenario. -+ struct Scenario { -+ int line_; // Scenario line number -+ string json_; // JSON configuration to parse -+ string exp_message_; // Expected exception message -+ }; -+ -+ // List of test scenarios to run. Most scenario supply -+ // all valid parameters except one in error. This allows -+ // us to verify that no values are changed if any are in error. -+ list<Scenario> scenarios = { -+ { -+ __LINE__, -+ R"( -+ { -+ "enable-ping-check" : false, -+ "min-ping-requests" : 3, -+ "reply-timeout" : 250, -+ "ping-cltt-secs" : 90, -+ "ping-channel-threads" : 4, -+ "bogus" : false -+ })", -+ "spurious 'bogus' parameter" -+ }, -+ { -+ __LINE__, -+ R"( -+ { -+ "enable-ping-check" : "not bool", -+ "min-ping-requests" : 3, -+ "reply-timeout" : 250, -+ "ping-cltt-secs" : 90, -+ "ping-channel-threads" : 4 -+ })", -+ "'enable-ping-check' parameter is not a boolean" -+ }, -+ { -+ __LINE__, -+ R"( -+ { -+ "enable-ping-check" : false, -+ "min-ping-requests" : 0, -+ "reply-timeout" : 250, -+ "ping-cltt-secs" : 90, -+ "ping-channel-threads" : 4 -+ })", -+ "invalid min-ping-requests: '0', must be greater than 0" -+ }, -+ { -+ __LINE__, -+ R"( -+ { -+ "enable-ping-check" : false, -+ "min-ping-requests" : -2, -+ "reply-timeout" : 250, -+ "ping-cltt-secs" : 90, -+ "ping-channel-threads" : 4 -+ })", -+ "invalid min-ping-requests: '-2', must be greater than 0" -+ }, -+ { -+ __LINE__, -+ R"( -+ { -+ "enable-ping-check" : false, -+ "min-ping-requests" : 1, -+ "reply-timeout" : 0, -+ "ping-cltt-secs" : 90, -+ "ping-channel-threads" : 4 -+ })", -+ "invalid reply-timeout: '0', must be greater than 0" -+ }, -+ { -+ __LINE__, -+ R"( -+ { -+ "enable-ping-check" : false, -+ "min-ping-requests" : 1, -+ "reply-timeout" : -77, -+ "ping-cltt-secs" : 90, -+ "ping-channel-threads" : 4 -+ })", -+ "invalid reply-timeout: '-77', must be greater than 0" -+ }, -+ { -+ __LINE__, -+ R"( -+ { -+ "enable-ping-check" : false, -+ "min-ping-requests" : 1, -+ "reply-timeout" : 250, -+ "ping-cltt-secs" : -3, -+ "ping-channel-threads" : 4 -+ })", -+ "invalid ping-cltt-secs: '-3', cannot be less than 0" -+ }, -+ { -+ __LINE__, -+ R"( -+ { -+ "enable-ping-check" : false, -+ "min-ping-requests" : 1, -+ "reply-timeout" : 250, -+ "ping-cltt-secs" : 90, -+ "ping-channel-threads" : -1 -+ })", -+ "invalid ping-channel-threads: '-1', cannot be less than 0" -+ } -+ }; -+ -+ // Iterate over the scenarios. -+ PingCheckConfig default_config; -+ for (auto const& scenario : scenarios) { -+ stringstream oss; -+ oss << "scenario at line: " << scenario.line_; -+ SCOPED_TRACE(oss.str()); -+ -+ // Convert JSON text to a map of parameters. -+ ConstElementPtr json_elements; -+ ASSERT_NO_THROW_LOG(json_elements = Element::fromJSON(scenario.json_)); -+ -+ // Parsing parameters should throw. -+ PingCheckConfig config; -+ ASSERT_THROW_MSG(config.parse(json_elements), dhcp::DhcpConfigError, -+ scenario.exp_message_); -+ -+ // Original values should be intact. -+ EXPECT_EQ(default_config.getEnablePingCheck(), config.getEnablePingCheck()); -+ EXPECT_EQ(default_config.getMinPingRequests(), config.getMinPingRequests()); -+ EXPECT_EQ(default_config.getReplyTimeout(), config.getReplyTimeout()); -+ EXPECT_EQ(default_config.getPingClttSecs(), config.getPingClttSecs()); -+ EXPECT_EQ(default_config.getPingChannelThreads(), config.getPingChannelThreads()); -+ } -+} -+ -+} // end of anonymous namespace -diff --git a/src/hooks/dhcp/ping_check/tests/ping_check_mgr_unittests.cc b/src/hooks/dhcp/ping_check/tests/ping_check_mgr_unittests.cc -new file mode 100644 -index 0000000000..ded13b085c ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/tests/ping_check_mgr_unittests.cc -@@ -0,0 +1,1878 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+/// @file This file contains tests which exercise the PingCheckMgr class. -+#include <config.h> -+ -+#include <ping_check_mgr.h> -+#include <ping_test_utils.h> -+#include <cc/data.h> -+#include <dhcp/pkt4.h> -+#include <dhcpsrv/cfgmgr.h> -+#include <dhcpsrv/lease.h> -+#include <hooks/hooks_manager.h> -+#include <util/chrono_time_utils.h> -+#include <testutils/gtest_utils.h> -+#include <testutils/multi_threading_utils.h> -+ -+#include <gtest/gtest.h> -+#include <mutex> -+#include <chrono> -+ -+using namespace std; -+using namespace isc; -+using namespace isc::data; -+using namespace isc::dhcp; -+using namespace isc::util; -+using namespace isc::asiolink; -+using namespace isc::ping_check; -+using namespace isc::hooks; -+using namespace isc::test; -+using namespace std::chrono; -+using namespace boost::asio::error; -+ -+namespace ph = std::placeholders; -+ -+namespace { -+ -+// Sanity check the basics for production class, PingCheckMgr, single-threaded mode. -+TEST(PingCheckMgr, basicsST) { -+ SKIP_IF(IOServiceTest::notRoot()); -+ MultiThreadingMgr::instance().setMode(false); -+ -+ // Create a multi-threaded manager. -+ IOServicePtr main_ios(new IOService()); -+ PingCheckMgrPtr mgr; -+ ASSERT_NO_THROW_LOG(mgr.reset(new PingCheckMgr(0))); -+ ASSERT_TRUE(mgr); -+ mgr->setIOService(main_ios); -+ -+ // Sanity check the global configuration. More robust tests are done -+ // elsewhere. -+ auto& config = mgr->getGlobalConfig(); -+ EXPECT_TRUE(config->getEnablePingCheck()); -+ EXPECT_EQ(1, config->getMinPingRequests()); -+ EXPECT_EQ(100, config->getReplyTimeout()); -+ EXPECT_EQ(60, config->getPingClttSecs()); -+ EXPECT_EQ(0, config->getPingChannelThreads()); -+ -+ // Verify we report as stopped. -+ EXPECT_FALSE(mgr->isRunning()); -+ EXPECT_TRUE(mgr->isStopped()); -+ EXPECT_FALSE(mgr->isPaused()); -+ -+ // Starting it should be OK. -+ ASSERT_NO_THROW_LOG(mgr->start()); -+ -+ // Verify we report as running. -+ EXPECT_TRUE(mgr->isRunning()); -+ EXPECT_FALSE(mgr->isStopped()); -+ EXPECT_FALSE(mgr->isPaused()); -+ -+ // Pausing it should be harmless. -+ ASSERT_NO_THROW_LOG(mgr->pause()); -+ -+ // Verify we report as running. -+ EXPECT_TRUE(mgr->isRunning()); -+ EXPECT_FALSE(mgr->isStopped()); -+ EXPECT_FALSE(mgr->isPaused()); -+ -+ // Resuming it should be harmless. -+ ASSERT_NO_THROW_LOG(mgr->resume()); -+ -+ // Verify we report as running. -+ EXPECT_TRUE(mgr->isRunning()); -+ EXPECT_FALSE(mgr->isStopped()); -+ EXPECT_FALSE(mgr->isPaused()); -+ -+ // Stopping it should be fine -+ ASSERT_NO_THROW_LOG(mgr->stop()); -+ -+ // Verify we report as stopped. -+ EXPECT_FALSE(mgr->isRunning()); -+ EXPECT_TRUE(mgr->isStopped()); -+ EXPECT_FALSE(mgr->isPaused()); -+ -+ // Re-starting it should be OK. -+ ASSERT_NO_THROW_LOG(mgr->start()); -+ -+ // Verify we report as running. -+ EXPECT_TRUE(mgr->isRunning()); -+ EXPECT_FALSE(mgr->isStopped()); -+ EXPECT_FALSE(mgr->isPaused()); -+ -+ // Calling destructor when its running should be OK. -+ ASSERT_NO_THROW_LOG(mgr.reset()); -+ -+ main_ios->stopAndPoll(); -+} -+ -+// Sanity check the basics for production class, PingCheckMgr. Bulk of testing -+// is done with test derivation, TestPingCheckMgr. -+TEST(PingCheckMgr, basicsMT) { -+ SKIP_IF(IOServiceTest::notRoot()); -+ MultiThreadingTest mt; -+ -+ // Create a multi-threaded manager. -+ IOServicePtr main_ios(new IOService()); -+ PingCheckMgrPtr mgr; -+ ASSERT_NO_THROW_LOG(mgr.reset(new PingCheckMgr(3))); -+ ASSERT_TRUE(mgr); -+ mgr->setIOService(main_ios); -+ -+ // Sanity check the global configuration. More robust tests are done -+ // elsewhere. -+ auto& config = mgr->getGlobalConfig(); -+ EXPECT_TRUE(config->getEnablePingCheck()); -+ EXPECT_EQ(1, config->getMinPingRequests()); -+ EXPECT_EQ(100, config->getReplyTimeout()); -+ EXPECT_EQ(60, config->getPingClttSecs()); -+ EXPECT_EQ(3, config->getPingChannelThreads()); -+ -+ // It should not be running yet. -+ EXPECT_FALSE(mgr->isRunning()); -+ EXPECT_TRUE(mgr->isStopped()); -+ EXPECT_FALSE(mgr->isPaused()); -+ -+ // Starting it should be OK. -+ ASSERT_NO_THROW_LOG(mgr->start()); -+ -+ // Verify it's running. -+ EXPECT_TRUE(mgr->isRunning()); -+ EXPECT_FALSE(mgr->isStopped()); -+ EXPECT_FALSE(mgr->isPaused()); -+ -+ // Pausing it should be fine. -+ ASSERT_NO_THROW_LOG(mgr->pause()); -+ -+ // Verify it's paused. -+ EXPECT_FALSE(mgr->isRunning()); -+ EXPECT_FALSE(mgr->isStopped()); -+ EXPECT_TRUE(mgr->isPaused()); -+ -+ // Resuming it should be fine. -+ ASSERT_NO_THROW_LOG(mgr->resume()); -+ -+ // Verify it's running. -+ EXPECT_TRUE(mgr->isRunning()); -+ EXPECT_FALSE(mgr->isStopped()); -+ EXPECT_FALSE(mgr->isPaused()); -+ -+ // Stopping it should be fine -+ ASSERT_NO_THROW_LOG(mgr->stop()); -+ -+ // It should not be running. -+ EXPECT_FALSE(mgr->isRunning()); -+ EXPECT_TRUE(mgr->isStopped()); -+ EXPECT_FALSE(mgr->isPaused()); -+ -+ // Re-starting it should be OK. -+ ASSERT_NO_THROW_LOG(mgr->start()); -+ -+ // Verify it's running. -+ EXPECT_TRUE(mgr->isRunning()); -+ EXPECT_FALSE(mgr->isStopped()); -+ EXPECT_FALSE(mgr->isPaused()); -+ -+ // Calling destructor when its running should be OK. -+ ASSERT_NO_THROW_LOG(mgr.reset()); -+} -+ -+// Verify basic behavior of PingCheckMgr::configure(). -+TEST(PingCheckMgr, configure) { -+ // Create a manager. -+ IOServicePtr main_ios(new IOService()); -+ PingCheckMgrPtr mgr; -+ ASSERT_NO_THROW_LOG(mgr.reset(new PingCheckMgr())); -+ ASSERT_TRUE(mgr); -+ -+ // Verify initial global configuration. -+ auto& default_config = mgr->getGlobalConfig(); -+ EXPECT_TRUE(default_config->getEnablePingCheck()); -+ EXPECT_EQ(1, default_config->getMinPingRequests()); -+ EXPECT_EQ(100, default_config->getReplyTimeout()); -+ EXPECT_EQ(60, default_config->getPingClttSecs()); -+ EXPECT_EQ(0, default_config->getPingChannelThreads()); -+ -+ //Create a valid configuration. -+ std::string valid_json_cfg = -+ R"({ -+ "enable-ping-check" : false, -+ "min-ping-requests" : 2, -+ "reply-timeout" : 250, -+ "ping-cltt-secs" : 90, -+ "ping-channel-threads" : 3 -+ })"; -+ -+ auto parameters = Element::fromJSON(valid_json_cfg); -+ -+ // Parse it. -+ ASSERT_NO_THROW_LOG(mgr->configure(parameters)); -+ -+ // Verify updated global configuration. -+ auto& config = mgr->getGlobalConfig(); -+ ASSERT_TRUE(config); -+ EXPECT_FALSE(config->getEnablePingCheck()); -+ EXPECT_EQ(2, config->getMinPingRequests()); -+ EXPECT_EQ(250, config->getReplyTimeout()); -+ EXPECT_EQ(90, config->getPingClttSecs()); -+ EXPECT_EQ(3, config->getPingChannelThreads()); -+ -+ // Create an invalid configuration. -+ std::string invalid_json_cfg = -+ R"({ -+ "enable-ping-check" : true, -+ "min-ping-requests" : 4, -+ "reply-timeout" : 500, -+ "ping-cltt-secs" : 45, -+ "ping-channel-threads" : 6, -+ "bogus" : 0 -+ })"; -+ -+ parameters = Element::fromJSON(invalid_json_cfg); -+ -+ // Parsing it should throw. -+ ASSERT_THROW_MSG(mgr->configure(parameters), DhcpConfigError, "spurious 'bogus' parameter"); -+ -+ // Verify configuration values were left unchanged. -+ auto& final_config = mgr->getGlobalConfig(); -+ ASSERT_TRUE(final_config); -+ EXPECT_EQ(final_config->getEnablePingCheck(), config->getEnablePingCheck()); -+ EXPECT_EQ(final_config->getMinPingRequests(), config->getMinPingRequests()); -+ EXPECT_EQ(final_config->getReplyTimeout(), config->getReplyTimeout()); -+ EXPECT_EQ(final_config->getPingClttSecs(), config->getPingClttSecs()); -+ EXPECT_EQ(final_config->getPingChannelThreads(), config->getPingChannelThreads()); -+} -+ -+/// @brief Defines a callback to invoke at the bottom of sendCompleted() -+typedef std::function<void(const ICMPMsgPtr& echo, bool send_failed)> SendCompletedCallback; -+ -+/// @brief Defines a callback to invoke at the bottom of replyReceived() -+typedef std::function<void(const ICMPMsgPtr& reply)> ReplyReceivedCallback; -+ -+/// @brief Testable derivation of PingCheckMgr -+/// -+/// Uses a TestablePingChannel to facilitate more robust testing. -+class TestablePingCheckMgr : public PingCheckMgr { -+public: -+ /// @brief Constructor. -+ /// -+ /// @param num_threads number of threads to use in the thread pool (0 means follow -+ /// core thread pool size) -+ /// @param min_echos minimum number of ECHO REQUESTs sent without replies -+ /// received required to declare an address free to offer. Defaults to 1, -+ /// must be greater than zero. -+ /// @param reply_timeout maximum number of milliseconds to wait for an -+ /// ECHO REPLY after an ECHO REQUEST has been sent. Defaults to 100, -+ TestablePingCheckMgr(uint32_t num_threads, uint32_t min_echos = 1, -+ uint32_t reply_timeout = 100) -+ : PingCheckMgr(num_threads, min_echos, reply_timeout), -+ post_send_completed_cb_(SendCompletedCallback()), -+ post_reply_received_cb_(ReplyReceivedCallback()) { -+ } -+ -+ /// @brief Destructor. -+ virtual ~TestablePingCheckMgr() { -+ post_send_completed_cb_ = SendCompletedCallback(); -+ post_reply_received_cb_ = ReplyReceivedCallback(); -+ if (getIOService()) { -+ getIOService()->stopAndPoll(); -+ } -+ } -+ -+ /// @brief Fetch the current channel instance. -+ /// -+ /// @return pointer to the TestablePingChannel instance (or an empty pointer). -+ TestablePingChannelPtr getChannel() { -+ return (boost::dynamic_pointer_cast<TestablePingChannel>(channel_)); -+ } -+ -+ /// @brief Fetches the manager's context store. -+ /// -+ /// @return Pointer to the PingContextStore. -+ PingContextStorePtr getStore() { -+ return (store_); -+ } -+ -+ /// @brief Fetches the expiration timer's current interval (in milliseconds). -+ /// -+ /// @return current interval as long or 0L if the timer is not currently -+ /// running or does not exist. -+ long getExpirationTimerInterval() { -+ if (expiration_timer_) { -+ return (expiration_timer_->getInterval()); -+ } -+ -+ return (0); -+ } -+ -+protected: -+ /// @brief Creates a TestablePingChannel instance. -+ /// -+ /// This override the base case creator. -+ /// -+ /// @param io_service IOService that will drive the channel. -+ /// @return pointer to the newly created channel. -+ virtual PingChannelPtr createChannel(asiolink::IOServicePtr io_service) { -+ return (TestablePingChannelPtr( -+ new TestablePingChannel(io_service, -+ std::bind(&PingCheckMgr::nextToSend, this, ph::_1), -+ std::bind(&TestablePingCheckMgr::sendCompleted, -+ this, ph::_1, ph::_2), -+ std::bind(&TestablePingCheckMgr::replyReceived, this, ph::_1), -+ std::bind(&PingCheckMgr::channelShutdown, this)))); -+ } -+ -+public: -+ /// @brief Fetches the current size of the parking lot. -+ /// -+ /// @return size_t containing the number of entries parked. -+ size_t parkingLotSize() const { -+ auto const& parking_lot = ServerHooks::getServerHooks().getParkingLotPtr("lease4_offer"); -+ return (parking_lot->size()); -+ } -+ -+ /// @brief Callback passed to PingChannel to invoke when an ECHO REQUEST -+ /// send has completed. -+ /// -+ /// -# Invokes the base class implementation -+ /// -# Invokes an optional callback -+ /// -+ /// @param echo ICMP echo message that is sent. -+ /// @param send_failed True if the send completed with a non-fatal error, -+ /// false otherwise. -+ virtual void sendCompleted(const ICMPMsgPtr& echo, bool send_failed) { -+ // Call the production callback. -+ PingCheckMgr::sendCompleted(echo, send_failed); -+ -+ // Invoke the post check, if one. -+ if (post_send_completed_cb_) { -+ (post_send_completed_cb_)(echo, send_failed); -+ } -+ } -+ -+ /// @brief Callback invoked by the channel to process received ICMP messages. -+ /// -+ /// -# Invokes the base class implementation -+ /// -# Pauses the test IOService thread and returns if the parking lot is empty -+ /// -# Invokes an option callback passing in the reply received -+ /// -+ /// @param reply pointer to the ICMP message received. -+ virtual void replyReceived(const ICMPMsgPtr& reply) { -+ if (reply->getType() == ICMPMsg::ECHO_REQUEST) { -+ return; -+ } -+ -+ // If we're routing loopback messages, look up the original address based -+ // on the sequence number and use it as the reply's source address. -+ if (getChannel()->route_loopback_) { -+ IOAddress address = getChannel()->loopback_map_.find(reply->getSequence()); -+ if (address != IOAddress::IPV4_ZERO_ADDRESS()) { -+ reply->setSource(address); -+ } -+ } -+ -+ // Call the production callback. -+ PingCheckMgr::replyReceived(reply); -+ -+ // Invoke the post check, if one. -+ if (post_reply_received_cb_) { -+ (post_reply_received_cb_)(reply); -+ } -+ } -+ -+ /// @brief Fetches the thread pool (if it exists). -+ /// -+ /// @return pointer to theIoServiceThreadPool. Will be empty -+ /// in ST mode or if the manager has not been started. -+ asiolink::IoServiceThreadPoolPtr getThreadPool() { -+ return (thread_pool_); -+ } -+ -+ /// @brief Sets the network_state object. -+ /// -+ /// @param network_state pointer to a NetworkState instance. -+ void setNetworkState(NetworkStatePtr network_state) { -+ network_state_ = network_state; -+ } -+ -+ /// @brief Callback to invoke at the bottom of sendCompleted(). -+ SendCompletedCallback post_send_completed_cb_; -+ -+ /// @brief Callback to invoke at the bottom of replyReceived(). -+ ReplyReceivedCallback post_reply_received_cb_; -+}; -+ -+/// @brief Defines a shared pointer to a PingCheckMgr. -+typedef boost::shared_ptr<TestablePingCheckMgr> TestablePingCheckMgrPtr; -+ -+/// @brief Holds a lease and its associated query. -+struct LeaseQueryPair { -+public: -+ /// @brief Constructor. -+ /// -+ /// @param lease pointer to the lease. -+ /// @param query pointer to the query. -+ LeaseQueryPair(Lease4Ptr lease, Pkt4Ptr query) : lease_(lease), query_(query) { -+ }; -+ -+ /// @brief Pointer to the lease. -+ Lease4Ptr lease_; -+ -+ /// @brief Pointer to the query. -+ Pkt4Ptr query_; -+}; -+ -+/// @brief Container of leases and their associated queries. -+typedef std::vector<LeaseQueryPair> LeaseQueryPairs; -+ -+/// @brief Test fixture for exercising PingCheckMgr. -+/// -+/// Uses a TestablePingCheckMgr instance for all tests and -+/// provides numerous helper functions. -+class PingCheckMgrTest : public IOServiceTest { -+public: -+ /// @brief Constructor. -+ PingCheckMgrTest() : mgr_(), lease_query_pairs_(), mutex_(new mutex()), -+ test_start_time_(PingContext::now()), unparked_(0) { -+ MultiThreadingMgr::instance().setMode(false); -+ }; -+ -+ /// @brief Destructor. -+ virtual ~PingCheckMgrTest() { -+ test_timer_.cancel(); -+ test_io_service_->stopAndPoll(); -+ MultiThreadingMgr::instance().setMode(false); -+ } -+ -+ /// @brief Pretest setup. -+ /// -+ /// Registers the hook point and creates its parking lot. -+ virtual void SetUp() { -+ HooksManager::registerHook("lease4_offer"); -+ parking_lot_ = boost::make_shared<ParkingLotHandle>( -+ ServerHooks::getServerHooks().getParkingLotPtr("lease4_offer")); -+ } -+ -+ /// @brief Ensure we stop cleanly. -+ virtual void TearDown() { -+ if (mgr_) { -+ mgr_->stop(); -+ } -+ -+ HooksManager::clearParkingLots(); -+ } -+ -+ /// @brief Creates the test's manager instance. -+ /// -+ /// @param num_threads number of threads in the thread pool. -+ /// @param min_echos minimum number of echos per ping check. -+ /// @param reply_timeout reply timeout per ping. -+ /// @param start_and_pause when false, the manager is only created, -+ /// when true it is created, started and then paused. This allows -+ /// manipulation of context store contents while the threads are doing -+ /// no work. -+ void createMgr(uint32_t num_threads, -+ uint32_t min_echos = 1, -+ uint32_t reply_timeout = 100, -+ bool start_and_pause = false) { -+ ASSERT_NO_THROW_LOG( -+ mgr_.reset(new TestablePingCheckMgr(num_threads, min_echos, reply_timeout))); -+ ASSERT_TRUE(mgr_); -+ mgr_->setIOService(test_io_service_); -+ -+ if (start_and_pause) { -+ ASSERT_NO_THROW_LOG(mgr_->start()); -+ -+ if (!MultiThreadingMgr::instance().getMode()) { -+ ASSERT_FALSE(mgr_->getThreadPool()); -+ } else { -+ ASSERT_TRUE(mgr_->getThreadPool()); -+ ASSERT_NO_THROW_LOG(mgr_->pause()); -+ ASSERT_TRUE(mgr_->isPaused()); -+ } -+ } -+ } -+ -+ /// @brief Add a new lease and query pair to the test's list of lease query pairs. -+ /// -+ /// Creates a bare-bones DHCPv4 lease and DHCPDISCOVER, wraps them in a -+ /// LeaseQueryPair and adds the pair to the end of the test's internal -+ /// list of pairs, lease_query_pairs_. -+ /// -+ /// @param target IOAddress of the lease. -+ /// @param transid transaction id of the query. -+ /// -+ /// @return A copy of the newly created pair. -+ LeaseQueryPair makeLeaseQueryPair(IOAddress target, uint16_t transid) { -+ // Make a lease and query pair -+ Lease4Ptr lease(new Lease4()); -+ lease->addr_ = IOAddress(target); -+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, transid)); -+ LeaseQueryPair lqp(lease, query); -+ lease_query_pairs_.push_back(lqp); -+ return (lqp); -+ } -+ -+ /// @brief Start ping checks for a given number of targets. -+ /// -+ /// The function first creates and parks the given number of targets, and -+ /// then starts a ping check for each of them. Parking them all first -+ /// establishes the number of ping checks expected to be conducted during -+ /// the test prior to actually starting any of them. This avoids the -+ /// parking lot from becoming empty part way through the test. -+ /// -+ /// It unpark callback lambda increments the unparked_ counter and then -+ /// pushes the unparked lease/query pair to either the list of frees -+ /// or list of declines. -+ /// -+ /// @param num_targets number of target ip addresses to ping check. -+ /// @param start_address starting target address. Defaults to 127.0.0.1. -+ /// -+ /// @return last target address started. -+ IOAddress startTargets(size_t num_targets, IOAddress start_address = IOAddress("127.0.0.1")) { -+ IOAddress target = start_address; -+ for (auto i = 0; i < num_targets; ++i) { -+ auto lqp = makeLeaseQueryPair(IOAddress(target), i+1); -+ HooksManager::park("lease4_offer", lqp.query_, -+ [this, lqp]() { -+ MultiThreadingLock lock(*mutex_); -+ ++unparked_; -+ auto handle = lqp.query_->getCalloutHandle(); -+ bool offer_address_in_use; -+ handle->getArgument("offer_address_in_use", offer_address_in_use); -+ offer_address_in_use ? declines_.push_back(lqp) : frees_.push_back(lqp); -+ }); -+ -+ try { -+ mgr_->startPing(lqp.lease_, lqp.query_, parking_lot_); -+ } catch (const std::exception& ex) { -+ ADD_FAILURE() << "startPing threw: " << ex.what(); -+ } -+ -+ target = IOAddress::increase(target); -+ } -+ -+ return(target); -+ } -+ -+ /// @brief Fetches the context, by lease address, from the store for a -+ /// given lease query pair. -+ /// -+ /// @param lqp LeaseQueryPair for which the context is desired. -+ /// @return pointer to the found context or an empty pointer. -+ PingContextPtr getContext(const LeaseQueryPair& lqp) { -+ return (getContext(lqp.lease_->addr_)); -+ } -+ -+ /// @brief Fetches the context, by lease address, from the store for address. -+ /// -+ /// @param address lease ip address for which the context is desired. -+ /// @return pointer to the found context or an empty pointer. -+ PingContextPtr getContext(const IOAddress& address) { -+ return (mgr_->getStore()->getContextByAddress(address)); -+ } -+ -+ /// @brief Updates a context in the store. -+ /// -+ /// @param context context to update. -+ void updateContext(PingContextPtr& context) { -+ ASSERT_NO_THROW_LOG(mgr_->getStore()->updateContext(context)); -+ } -+ -+ /// @brief Tests equality of two timestamps within a given tolerance. -+ /// -+ /// The two time stamps are considered equal if the absolute value of their -+ /// difference is between 0 and the specified tolerance (inclusive). -+ /// -+ /// @param lhs first TimeStamp to compare. -+ /// @param rhs second TimeStamp to compare. -+ /// @param tolerance margin of difference allowed for equality in milliseconds. -+ /// Defaults to 10. -+ /// -+ /// @return True if the time stamps are "equal", false otherwise. -+ bool fuzzyEqual(const TimeStamp& lhs, const TimeStamp& rhs, long tolerance = 10) { -+ auto diff = abs(duration_cast<milliseconds>(lhs - rhs).count()); -+ return (diff >= 0 && diff <= tolerance); -+ } -+ -+ /// @brief Tests equality of two longs within a given tolerance. -+ /// -+ /// The two values are considered equal if the absolute value of their -+ /// difference is between 0 and the specified tolerance (inclusive). -+ /// -+ /// @param lhs first value to compare. -+ /// @param rhs second value to compare. -+ /// @param tolerance margin of difference allowed for equality in milliseconds. -+ /// Defaults to 10. -+ /// -+ /// @return True if the time values are "equal", false otherwise. -+ bool fuzzyEqual(const long& lhs, const long& rhs, long tolerance = 10) { -+ auto diff = abs(lhs - rhs); -+ return (diff >= 0 && diff <= tolerance); -+ } -+ -+ /// @brief Creates an ECHO REQUEST message from a given address. -+ /// -+ /// @param target ip address to use as the echo's destination address. -+ /// @return Pointer to the new message. -+ ICMPMsgPtr makeEchoRequest(const IOAddress& target) { -+ ICMPMsgPtr msg(new ICMPMsg()); -+ msg->setType(ICMPMsg::ECHO_REQUEST); -+ msg->setDestination(IOAddress(target)); -+ msg->setSource(IOAddress("127.0.0.1")); -+ return (msg); -+ } -+ -+ /// @brief Creates an ECHO_REPLY message from a given address. -+ /// -+ /// @param from ip address to use as the reply's source address. -+ /// @return Pointer to the new message. -+ ICMPMsgPtr makeEchoReply(const IOAddress& from) { -+ ICMPMsgPtr msg(new ICMPMsg()); -+ msg->setType(ICMPMsg::ECHO_REPLY); -+ msg->setSource(IOAddress(from)); -+ msg->setDestination(IOAddress("127.0.0.1")); -+ return (msg); -+ } -+ -+ /// @brief Creates an TARGET_UNREACHABLE message from a given address. -+ /// -+ /// @param target ip address to use as the reply's source address. -+ /// @return Pointer to the new message. -+ ICMPMsgPtr makeUnreachable(const IOAddress& target) { -+ // Make the TARGET_UNREACHABLE message first. -+ ICMPMsgPtr msg(new ICMPMsg()); -+ msg->setType(ICMPMsg::TARGET_UNREACHABLE); -+ msg->setSource(IOAddress("127.0.0.1")); -+ msg->setDestination(IOAddress("127.0.0.1")); -+ -+ // Now embed the ping target's "original" echo into the unreachable -+ // message's payload. This includes the IP header followed by the -+ // ECHO REQUEST. First make the IP header and add it to the payload. -+ // We only set values we care about. -+ struct ip ip_header; -+ memset((void *)(&ip_header), 0x00, sizeof(struct ip)); -+ ip_header.ip_v = 4; -+ ip_header.ip_hl = 5; /* shift left twice = 20 */ -+ ip_header.ip_len = 48; /* ip_header + echo length */ -+ ip_header.ip_dst.s_addr = htonl(target.toUint32()); -+ ip_header.ip_src.s_addr = htonl(msg->getSource().toUint32()); -+ msg->setPayload((const uint8_t*)(&ip_header), sizeof(struct ip)); -+ -+ // Now make the ECHO_REQUEST, pack it and add that to the payload. -+ ICMPMsgPtr echo = makeEchoRequest(target); -+ ICMPPtr packed_echo = echo->pack(); -+ msg->setPayload((const uint8_t*)(packed_echo.get()), sizeof(struct icmp)); -+ -+ return (msg); -+ } -+ -+ /// @brief Compares a LeaseQueryPair collection to the internal collection -+ /// of pairs created (see makeLeaseQueryPairs()). -+ /// -+ /// @param test_collection Collection of pairs to compare against those in -+ /// the creation collection. -+ void compareLeaseQueryPairs(LeaseQueryPairs& test_collection) { -+ // We should have as many in the test_collection as we have creation -+ // collection. -+ ASSERT_EQ(test_collection.size(), lease_query_pairs_.size()); -+ -+ // Order is not guaranteed so we sort both lists then compare. -+ std::sort(test_collection.begin(), test_collection.end(), -+ [](LeaseQueryPair const& a, LeaseQueryPair const& b) -+ { return (a.lease_->addr_ < b.lease_->addr_); }); -+ -+ std::sort(lease_query_pairs_.begin(), lease_query_pairs_.end(), -+ [](LeaseQueryPair const& a, LeaseQueryPair const& b) -+ { return (a.lease_->addr_ < b.lease_->addr_); }); -+ -+ auto dpi = test_collection.begin(); -+ for (auto const& lqpi : lease_query_pairs_) { -+ ASSERT_EQ((*dpi).lease_->addr_, lqpi.lease_->addr_); -+ ++dpi; -+ } -+ } -+ -+ /// @brief Exercises the operational basics: create, start, and stop -+ /// for TestablePingCheckMgr. -+ /// -+ /// @param num_threads number of threads in the thread pool. -+ void testOperationalBasics(size_t num_threads) { -+ SKIP_IF(notRoot()); -+ -+ // Create manager with the given number of threads. -+ ASSERT_NO_THROW_LOG(createMgr(num_threads)); -+ ASSERT_TRUE(mgr_); -+ -+ // Should not be running. -+ EXPECT_FALSE(mgr_->isRunning()); -+ EXPECT_TRUE(mgr_->isStopped()); -+ EXPECT_FALSE(mgr_->isPaused()); -+ -+ // Channel should not yet exist. -+ ASSERT_FALSE(mgr_->getChannel()); -+ -+ // Start the manager. -+ ASSERT_NO_THROW_LOG(mgr_->start()); -+ -+ // Thread pool should exist in MT mode only. -+ if (MultiThreadingMgr::instance().getMode()) { -+ ASSERT_TRUE(mgr_->getThreadPool()); -+ } else { -+ ASSERT_FALSE(mgr_->getThreadPool()); -+ } -+ -+ // Should be running. -+ EXPECT_TRUE(mgr_->isRunning()); -+ EXPECT_FALSE(mgr_->isStopped()); -+ EXPECT_FALSE(mgr_->isPaused()); -+ -+ // Channel should exist and be open. -+ auto channel = mgr_->getChannel(); -+ ASSERT_TRUE(channel); -+ ASSERT_TRUE(channel->isOpen()); -+ -+ // Context store should exist and be empty. -+ auto store = mgr_->getStore(); -+ ASSERT_TRUE(store); -+ auto pings = store->getAll(); -+ ASSERT_EQ(0, pings->size()); -+ -+ // Destruction should be graceful. -+ ASSERT_NO_THROW_LOG(mgr_.reset()); -+ } -+ -+ /// @brief Verifies that startPing() creates a new context in the store and -+ /// it can be fetched with the nextToSend() callback. -+ void testStartPing() { -+ SKIP_IF(notRoot()); -+ -+ // Create manager with thread-pool size of 3, min_echos 2, reply_timeout 250 ms. -+ // ST mode should ingore requested thread number. -+ ASSERT_NO_THROW_LOG(createMgr(3, 2, 250)); -+ ASSERT_TRUE(mgr_); -+ -+ // Make a lease and query pair -+ auto lqp1 = makeLeaseQueryPair(IOAddress("127.0.0.101"), 101); -+ -+ // Channel isn't open, startPing should throw. -+ ASSERT_THROW_MSG(mgr_->startPing(lqp1.lease_, lqp1.query_, parking_lot_), InvalidOperation, -+ "PingCheckMgr::startPing() - channel isn't open"); -+ -+ // Start the manager. This will open the channel. -+ ASSERT_NO_THROW_LOG(mgr_->start()); -+ ASSERT_TRUE(mgr_->isRunning()); -+ -+ if (mgr_->getThreadPool()) { -+ // Pause the manager so startPing() will succeed but no events will occur. -+ // This should let us add contexts that sit in WAITING_TO_SEND state. -+ ASSERT_NO_THROW_LOG(mgr_->pause()); -+ ASSERT_TRUE(mgr_->isPaused()); -+ } -+ -+ // Call startPing() again. It should work. -+ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp1.lease_, lqp1.query_, parking_lot_)); -+ -+ // Calling startPing() on the same lease should fail, duplicates not allowed. -+ ASSERT_THROW_MSG(mgr_->startPing(lqp1.lease_, lqp1.query_, parking_lot_), DuplicateContext, -+ "PingContextStore::addContex: context already exists for: 127.0.0.101"); -+ -+ // Our context should be present. -+ auto const& store = mgr_->getStore(); -+ auto pings = store->getAll(); -+ ASSERT_EQ(1, pings->size()); -+ PingContextPtr context1; -+ ASSERT_NO_THROW_LOG(context1 = store->getContextByAddress(lqp1.lease_->addr_)); -+ ASSERT_TRUE(context1); -+ -+ // Verify the context's state. -+ EXPECT_EQ(2, context1->getMinEchos()); -+ EXPECT_EQ(250, context1->getReplyTimeout()); -+ EXPECT_EQ(0, context1->getEchosSent()); -+ EXPECT_EQ(PingContext::EMPTY_TIME(), context1->getLastEchoSentTime()); -+ EXPECT_LE(test_start_time_, context1->getSendWaitStart()); -+ EXPECT_EQ(PingContext::EMPTY_TIME(), context1->getNextExpiry()); -+ EXPECT_LE(test_start_time_, context1->getCreatedTime()); -+ EXPECT_EQ(lqp1.lease_, context1->getLease()); -+ EXPECT_EQ(lqp1.query_, context1->getQuery()); -+ EXPECT_EQ(PingContext::WAITING_TO_SEND, context1->getState()); -+ -+ // Sleep a bit to make sure there's a difference in context times. -+ usleep(5); -+ -+ // Make a second lease and query pair -+ auto lqp2 = makeLeaseQueryPair(IOAddress("127.0.0.102"), 102); -+ -+ // Start a ping for lease2. -+ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp2.lease_, lqp2.query_, parking_lot_)); -+ -+ // Both contexts should be present. -+ pings = store->getAll(); -+ ASSERT_EQ(2, pings->size()); -+ -+ // Fetch the second context by address. -+ PingContextPtr context2; -+ ASSERT_NO_THROW_LOG(context2 = store->getContextByAddress(lqp2.lease_->addr_)); -+ ASSERT_TRUE(context2); -+ -+ // Verify the second context's state. -+ EXPECT_EQ(2, context2->getMinEchos()); -+ EXPECT_EQ(250, context2->getReplyTimeout()); -+ EXPECT_EQ(0, context2->getEchosSent()); -+ EXPECT_EQ(PingContext::EMPTY_TIME(), context2->getLastEchoSentTime()); -+ // Its send_wait_start_time_ should be more recent than context1. -+ EXPECT_LE(context1->getSendWaitStart(), context2->getSendWaitStart()); -+ EXPECT_EQ(PingContext::EMPTY_TIME(), context2->getNextExpiry()); -+ // Its created_time_ should be more recent than context1. -+ EXPECT_LE(context1->getCreatedTime(), context2->getCreatedTime()); -+ EXPECT_EQ(lqp2.lease_, context2->getLease()); -+ EXPECT_EQ(lqp2.query_, context2->getQuery()); -+ EXPECT_EQ(PingContext::WAITING_TO_SEND, context2->getState()); -+ } -+ -+ /// @brief Exercises PingCheckMgr::nextToSend(). -+ void testNextToSend() { -+ SKIP_IF(notRoot()); -+ -+ // Create a paused manager. 3 threads, 2 echos, 250 ms timeout. -+ // ST mode should ingore requested thread number. -+ createMgr(3, 2, 250, true); -+ -+ // Calling nextToSend() should return false. -+ IOAddress next("0.0.0.0"); -+ ASSERT_FALSE(mgr_->nextToSend(next)); -+ -+ // Now let's start 3 contexts. -+ size_t num_targets = 3; -+ IOAddress target("127.0.0.1"); -+ for (auto i = 0; i < num_targets; ++i) { -+ auto lqp = makeLeaseQueryPair(IOAddress(target), i+1); -+ -+ // Call startPing(). -+ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp.lease_, lqp.query_, parking_lot_)); -+ target = IOAddress::increase(target); -+ -+ PingContextPtr context = getContext(lqp); -+ ASSERT_TRUE(context); -+ -+ // Verify the context's initial state is correct. -+ EXPECT_EQ(0, context->getEchosSent()); -+ EXPECT_EQ(PingContext::EMPTY_TIME(), context->getLastEchoSentTime()); -+ EXPECT_LE(test_start_time_, context->getSendWaitStart()); -+ EXPECT_EQ(PingContext::EMPTY_TIME(), context->getNextExpiry()); -+ EXPECT_LE(test_start_time_, context->getCreatedTime()); -+ EXPECT_EQ(PingContext::WAITING_TO_SEND, context->getState()); -+ -+ // Sleep a few before we add the next one to ensure ordering by -+ // time is consistent. -+ usleep(5); -+ } -+ -+ // Consecutive calls to nextToSend() should return target addresses -+ // in the order they were created. -+ for (auto const& lqp : lease_query_pairs_) { -+ // Next to send should return the next address to send. -+ ASSERT_TRUE(mgr_->nextToSend(next)); -+ -+ // It should match the lease as created. -+ ASSERT_EQ(next, lqp.lease_->addr_); -+ -+ // Fetch the corresponding context. -+ PingContextPtr context = getContext(next); -+ ASSERT_TRUE(context); -+ -+ // Verify the state has properly moved to SENDING. -+ EXPECT_EQ(0, context->getEchosSent()); -+ EXPECT_EQ(PingContext::EMPTY_TIME(), context->getLastEchoSentTime()); -+ EXPECT_EQ(PingContext::EMPTY_TIME(), context->getNextExpiry()); -+ EXPECT_EQ(PingContext::SENDING, context->getState()); -+ } -+ -+ // A final call to nextToSend should return false. -+ ASSERT_FALSE(mgr_->nextToSend(next)); -+ } -+ -+ /// @brief Exercises PingCheckMgr::setNextExpiration. -+ void testSetNextExpiration() { -+ SKIP_IF(notRoot()); -+ -+ // Create a paused manager. 3 threads, 2 echos, 500 ms timeout. -+ // ST mode should ingore requested thread number. -+ createMgr(3, 2, 500, true); -+ -+ // Should not have an expiration time, timer should not be running. -+ ASSERT_EQ(PingContext::EMPTY_TIME(), mgr_->getNextExpiry()); -+ ASSERT_EQ(mgr_->getExpirationTimerInterval(), 0); -+ -+ // Now let's start 3 contexts. -+ size_t num_targets = 3; -+ IOAddress target("127.0.0.1"); -+ for (auto i = 0; i < num_targets; ++i) { -+ auto lqp = makeLeaseQueryPair(IOAddress(target), i+1); -+ -+ // Call startPing(). -+ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp.lease_, lqp.query_, parking_lot_)); -+ target = IOAddress::increase(target); -+ } -+ -+ // Still should not have an expiration time nor running timer. -+ ASSERT_EQ(PingContext::EMPTY_TIME(), mgr_->getNextExpiry()); -+ EXPECT_EQ(mgr_->getExpirationTimerInterval(), 0); -+ -+ // Simulate a completed send for the second context. -+ PingContextPtr context2; -+ context2 = getContext(lease_query_pairs_[1]); -+ ASSERT_TRUE(context2); -+ context2->beginWaitingForReply(test_start_time_ - milliseconds(50)); -+ updateContext(context2); -+ -+ // Call setNextExpiration(). -+ ASSERT_NO_THROW_LOG(mgr_->setNextExpiration()); -+ -+ // Refresh the context. -+ context2 = getContext(lease_query_pairs_[1]); -+ -+ // Verify the mgr has the same next expiration as the context and -+ // that the expiration timer is running. Allow for some fudge in -+ // the checks. -+ auto original_mgr_expiry = mgr_->getNextExpiry(); -+ EXPECT_TRUE(fuzzyEqual(original_mgr_expiry, context2->getNextExpiry())); -+ -+ auto original_interval = mgr_->getExpirationTimerInterval(); -+ EXPECT_TRUE(fuzzyEqual(original_interval, 450)); -+ -+ // Simulate a completed send for the third context. -+ PingContextPtr context3; -+ context3 = getContext(lease_query_pairs_[2]); -+ ASSERT_TRUE(context3); -+ context3->beginWaitingForReply(); -+ updateContext(context3); -+ -+ // Call setNextExpiration(). -+ ASSERT_NO_THROW_LOG(mgr_->setNextExpiration()); -+ -+ // Refresh the context. -+ context3 = getContext(lease_query_pairs_[2]); -+ -+ // Context3 should have a later expiration than context2. -+ EXPECT_LT(context2->getNextExpiry(), context3->getNextExpiry()); -+ -+ // Expiration and timer should still match the original values based on -+ // the second context. -+ EXPECT_TRUE(fuzzyEqual(mgr_->getNextExpiry(), original_mgr_expiry)); -+ EXPECT_EQ(mgr_->getExpirationTimerInterval(), original_interval); -+ -+ // Simulate a completed send for the first context but use a smaller -+ // timeout and back date it. -+ PingContextPtr context1; -+ context1 = getContext(lease_query_pairs_[0]); -+ ASSERT_TRUE(context1); -+ context1->setReplyTimeout(50); -+ context1->beginWaitingForReply(test_start_time_ - milliseconds(1)); -+ updateContext(context1); -+ -+ // Call setNextExpiration(). -+ ASSERT_NO_THROW_LOG(mgr_->setNextExpiration()); -+ -+ // Refresh the context. -+ context1 = getContext(lease_query_pairs_[0]); -+ -+ // Context1 should have a earlier expiration than context2. -+ EXPECT_LT(context1->getNextExpiry(), context2->getNextExpiry()); -+ // Timer interval should be based on context1. -+ EXPECT_TRUE(fuzzyEqual(mgr_->getExpirationTimerInterval(), 50, 20)) -+ << " interval: " << mgr_->getExpirationTimerInterval(); -+ -+ // Move all contexts to TARGET_FREE. This should leave none -+ // still waiting. -+ context1->setState(PingContext::TARGET_FREE); -+ updateContext(context1); -+ context2->setState(PingContext::TARGET_FREE); -+ updateContext(context2); -+ context3->setState(PingContext::TARGET_FREE); -+ updateContext(context3); -+ -+ // Call setNextExpiration(). -+ ASSERT_NO_THROW_LOG(mgr_->setNextExpiration()); -+ -+ // Should not have an expiration time, timer should not be running. -+ ASSERT_EQ(PingContext::EMPTY_TIME(), mgr_->getNextExpiry()); -+ ASSERT_EQ(mgr_->getExpirationTimerInterval(), 0); -+ } -+ -+ /// @brief Exercises PingCheckMgr::sendCompleted. -+ void testSendCompleted() { -+ SKIP_IF(notRoot()); -+ -+ // Create a paused manager. 3 threads, 2 echos, 500 ms timeout. -+ // ST mode should ingore requested thread number. -+ createMgr(3, 2, 500, true); -+ -+ // Start a ping for an address so we have a context. -+ IOAddress target("127.0.0.2"); -+ auto lqp = makeLeaseQueryPair(IOAddress(target), 102); -+ -+ // Call startPing(). -+ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp.lease_, lqp.query_, parking_lot_)); -+ -+ // Simulate a completed send for the context. -+ PingContextPtr context; -+ context = getContext(lqp); -+ ASSERT_TRUE(context); -+ -+ // Make an ECHO REQUEST packet based on context. -+ ICMPMsgPtr echo_request = makeEchoRequest(context->getLease()->addr_); -+ -+ // Invoke sendCompleted() with fabricated request. Should succeed. -+ ASSERT_NO_THROW_LOG(mgr_->sendCompleted(echo_request, false)); -+ -+ // Refresh the context. -+ context = getContext(context->getLease()->addr_); -+ -+ EXPECT_EQ(PingContext::WAITING_FOR_REPLY, context->getState()); -+ EXPECT_EQ(1, context->getEchosSent()); -+ EXPECT_GE(context->getLastEchoSentTime(), test_start_time_); -+ -+ // Verify the mgr has the same next expiration as the context and -+ // that the expiration timer is running. Allow for some fudge in -+ // the checks. -+ EXPECT_GT(context->getNextExpiry(), test_start_time_); -+ EXPECT_TRUE(fuzzyEqual(mgr_->getNextExpiry(), context->getNextExpiry())); -+ EXPECT_TRUE(fuzzyEqual(mgr_->getExpirationTimerInterval(), 500)); -+ -+ // Make an ECHO REQUEST packet for an address that has no context. -+ echo_request = makeEchoRequest(IOAddress("192.168.0.1")); -+ -+ // Invoking sendCompleted() with request for a non-existent address be harmless. -+ ASSERT_NO_THROW_LOG(mgr_->sendCompleted(echo_request, false)); -+ -+ // Invoking sendCompleted() with an invalid message type should be harmless. -+ echo_request->setType(ICMPMsg::ECHO_REPLY); -+ ASSERT_NO_THROW_LOG(mgr_->sendCompleted(ICMPMsgPtr(), false)); -+ -+ // Invoking sendCompleted() with an empty message should be harmless. -+ echo_request.reset(); -+ ASSERT_NO_THROW_LOG(mgr_->sendCompleted(ICMPMsgPtr(), false)); -+ -+ // Verify expiration values should not have not been altered. -+ EXPECT_TRUE(fuzzyEqual(mgr_->getNextExpiry(), context->getNextExpiry())); -+ EXPECT_TRUE(fuzzyEqual(mgr_->getExpirationTimerInterval(), 500)); -+ } -+ -+ /// @brief Exercises PingCheckMgr::replyReceived() for ECHO REPLYs. Note this -+ /// also exercises handleEchoReply(). -+ void testReplyReceivedForEchoReply() { -+ SKIP_IF(notRoot()); -+ -+ // Create a paused manager. 3 threads, 2 echos, 500 ms timeout. -+ // ST mode should ingore requested thread number. -+ createMgr(3, 2, 500, true); -+ -+ // Install a post reply received callback to stop the test if we're done. -+ mgr_->post_reply_received_cb_ = -+ [this](const ICMPMsgPtr& /* reply */) { -+ MultiThreadingLock lock(*mutex_); -+ if (mgr_->parkingLotSize() == 0) { -+ stopTestService(); -+ return; -+ } -+ }; -+ -+ // Turn off loopback routing. -+ mgr_->getChannel()->route_loopback_ = false; -+ -+ // Start a ping for an address so we have a context. -+ startTargets(1); -+ auto lqp = lease_query_pairs_[0]; -+ -+ // Simulate a completed send for the context. -+ PingContextPtr context; -+ context = getContext(lqp); -+ ASSERT_TRUE(context); -+ -+ // Make an ECHO REQUEST packet based on context and invoke sendCompleted(). -+ ICMPMsgPtr echo_request = makeEchoRequest(context->getLease()->addr_); -+ ASSERT_NO_THROW_LOG(mgr_->sendCompleted(echo_request, false)); -+ -+ // Should still have one parked query. -+ EXPECT_EQ(1, mgr_->parkingLotSize()); -+ -+ // Verify the expiration timer is running. -+ EXPECT_TRUE(fuzzyEqual(mgr_->getExpirationTimerInterval(), 500)); -+ -+ // Make an ECHO REPLY packet based on context and invoke replyReceived(). -+ ICMPMsgPtr echo_reply = makeEchoReply(context->getLease()->addr_); -+ ASSERT_NO_THROW_LOG(mgr_->replyReceived(echo_reply)); -+ -+ // Verify the expiration timer is no longer running. -+ EXPECT_EQ(mgr_->getExpirationTimerInterval(), 0); -+ -+ // The context should no longer be in the store. -+ EXPECT_FALSE(getContext(lqp)); -+ -+ // We should have dropped the query from the lot rather than unparking it. -+ EXPECT_EQ(mgr_->parkingLotSize(), 0); -+ EXPECT_EQ(unparked_, 1); -+ -+ // We should have one decline that matches our lease query pair. -+ compareLeaseQueryPairs(declines_); -+ -+ // Make an ECHO REPLY packet for an address that has no context. -+ echo_reply = makeEchoReply(IOAddress("192.168.0.1")); -+ -+ // Invoke replyReceived() for a reply with no matching context, -+ // it should not throw. -+ ASSERT_NO_THROW_LOG(mgr_->PingCheckMgr::replyReceived(echo_reply)); -+ -+ // Invoke replyReceived() an empty message, it should not throw. -+ // (Bypass test implementation for this check). -+ echo_reply.reset(); -+ ASSERT_NO_THROW_LOG(mgr_->PingCheckMgr::replyReceived(echo_reply)); -+ } -+ -+ /// @brief Exercises PingCheckMgr::replyReceived() for UNREACHABLEs. Note this -+ /// also exercises handleTargetUnreachable(). -+ void testReplyReceivedForTargetUnreachable() { -+ SKIP_IF(notRoot()); -+ -+ // Create a paused manager. 3 threads, 2 echos, 500 ms timeout. -+ // ST mode should ingore requested thread number. -+ createMgr(3, 2, 500, true); -+ -+ // Install a post reply received callback to stop the test if we're done. -+ mgr_->post_reply_received_cb_ = -+ [this](const ICMPMsgPtr& /* reply */) { -+ MultiThreadingLock lock(*mutex_); -+ if (mgr_->parkingLotSize() == 0) { -+ stopTestService(); -+ return; -+ } -+ }; -+ -+ // Turn off loopback routing. -+ mgr_->getChannel()->route_loopback_ = false; -+ -+ // Start a ping for an address so we have a context. -+ startTargets(1); -+ auto lqp = lease_query_pairs_[0]; -+ -+ // Simulate a completed send for the context. -+ PingContextPtr context; -+ context = getContext(lqp); -+ ASSERT_TRUE(context); -+ -+ // Make an ECHO REQUEST packet based on context and invoke sendCompleted(). -+ ICMPMsgPtr echo_request = makeEchoRequest(context->getLease()->addr_); -+ ASSERT_NO_THROW_LOG(mgr_->sendCompleted(echo_request, false)); -+ -+ // Should still have one parked query. -+ EXPECT_EQ(1, mgr_->parkingLotSize()); -+ -+ // Verify the expiration timer is running. -+ EXPECT_TRUE(fuzzyEqual(mgr_->getExpirationTimerInterval(), 500)); -+ -+ // Make an ECHO REPLY packet based on context and invoke replyReceived(). -+ ICMPMsgPtr unreachable = makeUnreachable(context->getLease()->addr_); -+ ASSERT_NO_THROW_LOG(mgr_->replyReceived(unreachable)); -+ -+ // Verify the expiration timer is no longer running. -+ EXPECT_EQ(mgr_->getExpirationTimerInterval(), 0); -+ -+ // The context should no longer be in the store. -+ EXPECT_FALSE(getContext(lqp)); -+ -+ // We should have unparked the query from the lot. -+ EXPECT_EQ(mgr_->parkingLotSize(), 0); -+ EXPECT_EQ(unparked_, 1); -+ -+ // We should have one free that matches our lease query pair. -+ compareLeaseQueryPairs(frees_); -+ -+ // Invoke replyReceived() for an unreachable with no matching context, -+ // it should not throw. -+ unreachable = makeUnreachable(IOAddress("192.168.0.1")); -+ ASSERT_NO_THROW_LOG(mgr_->replyReceived(unreachable)); -+ } -+ -+ /// @brief Verifies expiration processing by invoking expirationTimedout(). -+ /// This also exercises processExpiredSince(), doNextEcho(), finishFree(), -+ /// and setNextExpiration(). -+ void testExpirationProcessing() { -+ SKIP_IF(notRoot()); -+ -+ // Create a paused manager. 3 threads, 1 echos, 250 ms timeout. -+ // ST mode should ingore requested thread number. -+ createMgr(3, 1, 250, true); -+ -+ // Start four ping checks, then stage them so: -+ // -+ // First context is WAITING_TO_SEND, no expiry. -+ // Second context is WAITING_FOR_REPLY, has expired and has -+ // exhausted min_echos_. -+ // Third context is WAITING_FOR_REPLY, has expired but has -+ // not exhausted min_echos_. -+ // Fourth context is WAITING_FOR_REPLY but has not yet expired. -+ // -+ size_t num_targets = 4; -+ -+ // Start the desired number of targets with an unpark callback -+ // that increments the unparked count. -+ startTargets(num_targets); -+ -+ // Now establish the desired state for each context. -+ // First context is in WAITING_TO_SEND, no expiry. -+ PingContextPtr context1 = getContext(lease_query_pairs_[0]); -+ ASSERT_TRUE(context1); -+ EXPECT_EQ(context1->getState(), PingContext::WAITING_TO_SEND); -+ -+ // Second context setup: expired and has exhausted min_echos_ -+ PingContextPtr context2 = getContext(lease_query_pairs_[1]); -+ ASSERT_TRUE(context2); -+ context2->beginWaitingForReply(test_start_time_ - milliseconds(500)); -+ updateContext(context2); -+ -+ // Third context setup: expired but has not exhausted min_echos_ -+ PingContextPtr context3 = getContext(lease_query_pairs_[2]); -+ ASSERT_TRUE(context3); -+ context3->setMinEchos(2); -+ context3->beginWaitingForReply(test_start_time_ - milliseconds(500)); -+ updateContext(context3); -+ -+ // Fourth context setup: has not yet expired -+ PingContextPtr context4 = getContext(lease_query_pairs_[3]); -+ ASSERT_TRUE(context4); -+ context4->beginWaitingForReply(test_start_time_); -+ updateContext(context4); -+ -+ // Now invoke expirationTimedout(). -+ ASSERT_NO_THROW_LOG(mgr_->expirationTimedOut()); -+ -+ // Verify the contexts are in the expected states. -+ // Context1 should still be WAITING_TO_SEND. -+ context1 = getContext(lease_query_pairs_[0]); -+ ASSERT_TRUE(context1); -+ EXPECT_EQ(context1->getState(), PingContext::WAITING_TO_SEND); -+ -+ // Context2 should be gone by unparking and its address freed. -+ IOAddress address = lease_query_pairs_[1].lease_->addr_; -+ context2 = getContext(address); -+ ASSERT_FALSE(context2); -+ EXPECT_EQ(unparked_, 1); -+ ASSERT_EQ(frees_.size(), 1); -+ EXPECT_EQ(frees_[0].lease_->addr_, address); -+ -+ // Context3 should be in WAITING_TO_SEND. -+ context3 = getContext(lease_query_pairs_[2]); -+ ASSERT_TRUE(context3); -+ EXPECT_EQ(context3->getState(), PingContext::WAITING_TO_SEND); -+ -+ // Context4 should still be WAITING_FOR_REPLY. -+ context4 = getContext(lease_query_pairs_[3]); -+ ASSERT_TRUE(context4); -+ EXPECT_EQ(context4->getState(), PingContext::WAITING_FOR_REPLY); -+ -+ // Manager's next_expiry_ should be based on context4? -+ EXPECT_TRUE(fuzzyEqual(mgr_->getNextExpiry(), context4->getNextExpiry())); -+ } -+ -+ /// @brief Generates a number of ping checks to local loop back addresses. -+ /// -+ /// Pings should all result in ECHO_REPLYs that get "declined". Declined -+ /// addresses are added to a list. Test completion is gated by the parking -+ /// lot becoming empty or test times out. -+ void testMultiplePingsWithReply() { -+ SKIP_IF(notRoot()); -+ -+ // Create manager with thread-pool size of 3, min_echos 1, -+ // reply_timeout 1000 milliseconds. Larger time out for this test -+ // avoids sporadic expirations which leads to unaccounted for UNPARKs. -+ // ST mode should ingore requested thread number. -+ ASSERT_NO_THROW_LOG(createMgr(3, 1, 1000)); -+ ASSERT_TRUE(mgr_); -+ -+ // Install a post reply received callback to stop the test if we're done. -+ int num_targets = 25; -+ mgr_->post_reply_received_cb_ = -+ [this, num_targets](const ICMPMsgPtr& /* reply */) { -+ MultiThreadingLock lock(*mutex_); -+ if (unparked_ == num_targets) { -+ stopTestService(); -+ return; -+ } -+ }; -+ -+ // Start the manager. This will open the channel. -+ ASSERT_NO_THROW_LOG(mgr_->start()); -+ ASSERT_TRUE(mgr_->isRunning()); -+ -+ // Start the ping checks. -+ startTargets(num_targets); -+ -+ // Run the main thread's IOService until we complete or timeout. -+ ASSERT_NO_THROW_LOG(runIOService()); -+ -+ // Stop the thread pool. -+ ASSERT_NO_THROW_LOG(mgr_->stop()); -+ ASSERT_TRUE(mgr_->isStopped()); -+ -+ // Calling nextToSend() should return false. -+ IOAddress next("0.0.0.0"); -+ ASSERT_FALSE(mgr_->nextToSend(next)); -+ -+ // We should have as many declines as we have pairs created. -+ compareLeaseQueryPairs(declines_); -+ } -+ -+ /// @brief Generates a large number of ping checks to local loop back addresses. -+ /// -+ /// A pause is induced approximately halfway through the number of replies -+ /// at which point the manager is paused and then resumed. This is intended -+ /// to demonstrate the ability to pause and resume the manager gracefully. -+ /// The pings should all result in ECHO_REPLYs that get "declined". Declined -+ /// addresses are added to a list. Test completion is gated by the parking -+ /// lot becoming empty or test times out. -+ void testMultiplePingsWithReplyAndPause() { -+ SKIP_IF(notRoot()); -+ -+ // Create manager with thread-pool size of 3, min_echos 1, -+ // reply_timeout 1000 milliseconds. Larger time out for this test -+ // avoids sporadic expirations which leads to unaccounted for UNPARKs. -+ // ST mode should ingore requested thread number. -+ ASSERT_NO_THROW_LOG(createMgr(3, 1, 1000)); -+ ASSERT_TRUE(mgr_); -+ -+ // Generate ping checks to the desired number of targets. -+ // Set up the pause callback to pause at half the number of -+ // expected replies. -+ size_t num_targets = 24; -+ size_t reply_cnt = 0; -+ size_t pause_at = num_targets / 2; -+ bool test_paused = false; -+ -+ // Install post reply callback to stop the test thread when we reach -+ // the pause count. -+ mgr_->post_reply_received_cb_ = -+ [this, &reply_cnt, &test_paused, &pause_at](const ICMPMsgPtr& reply) { -+ MultiThreadingLock lock(*mutex_); -+ if (reply->getType() == ICMPMsg::ECHO_REPLY) { -+ ++reply_cnt; -+ if (pause_at && (reply_cnt >= pause_at)) { -+ test_paused = true; -+ stopTestService(); -+ pause_at = 0; -+ } -+ } -+ }; -+ -+ // Start the manager. This will open the channel. -+ ASSERT_NO_THROW_LOG(mgr_->start()); -+ ASSERT_TRUE(mgr_->isRunning()); -+ ASSERT_NO_THROW_LOG(mgr_->pause()); -+ -+ // Start 1/2 desired number of ping checks. -+ startTargets(num_targets / 2); -+ -+ // Run the main thread's IOService until we pause or timeout. -+ ASSERT_NO_THROW_LOG(mgr_->resume()); -+ ASSERT_TRUE(mgr_->isRunning()); -+ ASSERT_NO_THROW_LOG(runIOService()); -+ -+ // Manager should still be running. Pause it. -+ ASSERT_TRUE(mgr_->isRunning()); -+ if (mgr_->getThreadPool()) { -+ ASSERT_NO_THROW_LOG(mgr_->pause()); -+ ASSERT_TRUE(mgr_->isPaused()); -+ } -+ -+ // Verify that the pause callback is why we stopped, that we -+ // received at least as many as we should have before pause -+ // and that we have more work to do. The test is a range as -+ // pausing does not happen exactly at the same point from test -+ // run to test run. -+ ASSERT_TRUE(test_paused); -+ ASSERT_TRUE((reply_cnt >= pause_at) && (reply_cnt < num_targets)) -+ << "reply_cnt " << reply_cnt -+ << ", pause_at " << pause_at -+ << ", num_targets " << num_targets; -+ -+ mgr_->post_reply_received_cb_ = -+ [this, num_targets](const ICMPMsgPtr& /* reply */) { -+ MultiThreadingLock lock(*mutex_); -+ if (unparked_ == num_targets) { -+ stopTestService(); -+ return; -+ } -+ }; -+ -+ // Start second batch of targets. -+ startTargets(num_targets / 2, IOAddress("127.0.0.15")); -+ -+ ASSERT_NO_THROW_LOG(mgr_->resume()); -+ ASSERT_TRUE(mgr_->isRunning()); -+ -+ // Restart the main thread's IOService until we complete or timeout. -+ ASSERT_NO_THROW_LOG(runIOService()); -+ -+ ASSERT_NO_THROW_LOG(mgr_->stop()); -+ ASSERT_TRUE(mgr_->isStopped()); -+ -+ // Calling nextToSend() should return false. -+ IOAddress next("0.0.0.0"); -+ ASSERT_FALSE(mgr_->nextToSend(next)); -+ -+ // We should have as many declines as we have pairs created. -+ compareLeaseQueryPairs(declines_); -+ } -+ -+ /// @brief Verifies that a recoverable error completion in sendCompleted() results -+ /// in the target address being free to use. In other words, it should have -+ /// the same outcome as the receiving a TARGET_UNREACHABLE reply from the OS. -+ void testSendCompletedSendFailed() { -+ SKIP_IF(notRoot()); -+ -+ // Create manager with thread-pool size of 3, min_echos 1, -+ // reply_timeout 250 milliseconds. -+ // ST mode should ingore requested thread number. -+ ASSERT_NO_THROW_LOG(createMgr(3, 1, 250)); -+ ASSERT_TRUE(mgr_); -+ -+ // Install a post send completed callback to stop the test if we're done. -+ mgr_->post_send_completed_cb_ = -+ [this](const ICMPMsgPtr& /* echo */, bool send_failed) { -+ MultiThreadingLock lock(*mutex_); -+ if (send_failed) { -+ stopTestService(); -+ } -+ }; -+ -+ // Start the manager. -+ ASSERT_NO_THROW_LOG(mgr_->start()); -+ -+ // Set the test channel to complete the first send with a network_unreachable -+ // error. This saves us from trying to determine an address in the test -+ // environment that would cause it. -+ mgr_->getChannel()->ec_on_write_number_ = 1; -+ mgr_->getChannel()->write_error_ec_ = make_error_code(network_unreachable); -+ -+ // Start a ping for one target. -+ startTargets(1); -+ auto lqp = lease_query_pairs_[0]; -+ -+ // Run the main thread's IOService until we complete or timeout. -+ ASSERT_NO_THROW_LOG(runIOService()); -+ -+ // Verify the expiration timer is no longer running. -+ EXPECT_EQ(mgr_->getExpirationTimerInterval(), 0); -+ -+ // The context should no longer be in the store. -+ EXPECT_FALSE(getContext(lqp)); -+ -+ // We should have unparked the query from the lot. -+ EXPECT_EQ(mgr_->parkingLotSize(), 0); -+ EXPECT_EQ(unparked_, 1); -+ -+ // We should have one free that matches our lease query pair. -+ compareLeaseQueryPairs(frees_); -+ } -+ -+ /// @brief Exercises shouldPing(). -+ void testShouldPingTest() { -+ SKIP_IF(notRoot()); -+ -+ // Create manager with thread-pool size of 3, min_echos 1, -+ // reply_timeout 250 milliseconds. -+ // ST mode should ingore requested thread number. -+ ASSERT_NO_THROW_LOG(createMgr(3, 1, 250)); -+ ASSERT_TRUE(mgr_); -+ -+ // Make a default config. -+ PingCheckConfigPtr config(new PingCheckConfig()); -+ -+ // Make a lease query pair. -+ auto lqp1 = makeLeaseQueryPair(IOAddress("127.0.0.2"), 111); -+ const uint8_t id1[] = { 0x31, 0x32, 0x33, 0x34 }; -+ ClientIdPtr cid1(new ClientId(id1, sizeof(id1))); -+ lqp1.lease_->client_id_ = cid1; -+ -+ Lease4Ptr empty_lease; -+ CalloutHandle::CalloutNextStep status; -+ -+ // Ping checking enabled, no old lease, channel doesn't exist, should return CONTINUE. -+ ASSERT_TRUE(config->getEnablePingCheck()); -+ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp1.lease_, lqp1.query_, empty_lease, config)); -+ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_CONTINUE); -+ -+ // Start the manager, then pause it. This lets us start pings without -+ // them changing state. -+ ASSERT_NO_THROW_LOG(mgr_->start()); -+ ASSERT_NO_THROW_LOG(mgr_->pause()); -+ -+ // Ping checking disabled, no old lease, should return CONTINUE. -+ config->setEnablePingCheck(false); -+ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp1.lease_, lqp1.query_, empty_lease, config)); -+ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_CONTINUE); -+ -+ // Ping checking enabled, no old lease, should return PARK. -+ config->setEnablePingCheck(true); -+ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp1.lease_, lqp1.query_, empty_lease, config)); -+ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_PARK); -+ -+ // Make an old lease based on the first lease. -+ time_t now = time(0); -+ Lease4Ptr old_lease(new Lease4(*(lqp1.lease_))); -+ -+ // Prior lease belonging to the same client with cltt greater than ping-cltt-secs -+ // should return PARK. -+ old_lease->cltt_ = now - config->getPingClttSecs() * 2; -+ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp1.lease_, lqp1.query_, old_lease, config)); -+ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_PARK); -+ -+ // Prior lease belonging to the same client but with cltt less than ping-cltt-secs -+ // should return CONTINUE. -+ old_lease->cltt_ = now - config->getPingClttSecs() / 2; -+ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp1.lease_, lqp1.query_, old_lease, config)); -+ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_CONTINUE); -+ -+ // Prior lease belonging to a different client, should return PARK. -+ const uint8_t id2[] = { 0x35, 0x36, 0x37, 0x34 }; -+ old_lease->client_id_.reset(new ClientId(id2, sizeof(id2))); -+ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp1.lease_, lqp1.query_, old_lease, config)); -+ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_PARK); -+ -+ // Now let's start a ping for the lease-query pair. -+ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp1.lease_, lqp1.query_, parking_lot_)); -+ -+ // Make a second lease query pair. Same address, different client. -+ auto lqp2 = makeLeaseQueryPair(IOAddress("127.0.0.2"), 333); -+ lqp2.lease_->client_id_ = old_lease->client_id_; -+ -+ // Trying to start a ping for an address already being checked should return DROP. -+ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp2.lease_, lqp2.query_, empty_lease, config)); -+ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_DROP); -+ -+ // Stop the mgr. -+ ASSERT_NO_THROW(mgr_->stop()); -+ -+ // Ping checking enabled, no old lease, channel isn't open, should return CONTINUE. -+ ASSERT_TRUE(config->getEnablePingCheck()); -+ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp1.lease_, lqp1.query_, empty_lease, config)); -+ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_CONTINUE); -+ } -+ -+ /// @brief Exercise's getScopedConfig(). -+ void testGetScopedConfig() { -+ CfgMgr::instance().setFamily(AF_INET); -+ -+ // Start with empty cache, any subnet that hasn't been seen should get parsed -+ // and, if valid, added to the cache. -+ CfgMgr& cfg_mgr = CfgMgr::instance(); -+ CfgSubnets4Ptr subnets = cfg_mgr.getStagingCfg()->getCfgSubnets4(); -+ -+ // Subnet 1 has no ping-check config. Should return global config. -+ ElementPtr user_context = Element::createMap(); -+ Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.1.0"), 24, 30, 40, 60, 1)); -+ subnet->setContext(user_context); -+ subnets->add(subnet); -+ -+ // Subnet 2 has invalid ping-check content. Should return global config. -+ std::string invalid_json_cfg = -+ R"({ -+ "ping-check": { -+ "enable-ping-check" : true, -+ "bogus-key-word" : true -+ } -+ })"; -+ -+ user_context = Element::fromJSON(invalid_json_cfg); -+ subnet.reset(new Subnet4(IOAddress("192.0.2.0"), 24, 30, 40, 60, 2)); -+ subnet->setContext(user_context); -+ subnets->add(subnet); -+ -+ // Subnet 3 has valid ping check. Should return subnet config -+ std::string valid_json_cfg = -+ R"({ -+ "ping-check": { -+ "enable-ping-check" : true, -+ "min-ping-requests" : 13 -+ } -+ })"; -+ -+ user_context = Element::fromJSON(valid_json_cfg); -+ subnet.reset(new Subnet4(IOAddress("192.0.3.0"), 24, 30, 40, 60, 3)); -+ subnet->setContext(user_context); -+ subnets->add(subnet); -+ -+ // Commit the subnet configuration. -+ cfg_mgr.commit(); -+ -+ // Create manager with thread-pool size of 3, min_echos 2, reply_timeout 250 ms. -+ ASSERT_NO_THROW_LOG(createMgr(3, 2, 250)); -+ ASSERT_TRUE(mgr_); -+ -+ Lease4Ptr lease(new Lease4()); -+ PingCheckConfigPtr config; -+ -+ // Should get the global configuration for subnet 1. -+ lease->addr_ = IOAddress("192.0.1.1"); -+ lease->subnet_id_ = 1; -+ ASSERT_NO_THROW_LOG(config = mgr_->getScopedConfig(lease)); -+ ASSERT_TRUE(config); -+ ASSERT_EQ(config, mgr_->getGlobalConfig()); -+ -+ // Should get the global configuration for subnet 2. -+ lease->addr_ = IOAddress("192.0.2.1"); -+ lease->subnet_id_ = 2; -+ ASSERT_NO_THROW_LOG(config = mgr_->getScopedConfig(lease)); -+ ASSERT_TRUE(config); -+ ASSERT_EQ(config, mgr_->getGlobalConfig()); -+ -+ // Should get subnet configuration for subnet 3. -+ lease->addr_ = IOAddress("192.0.3.1"); -+ lease->subnet_id_ = 3; -+ ASSERT_NO_THROW_LOG(config = mgr_->getScopedConfig(lease)); -+ ASSERT_TRUE(config); -+ ASSERT_NE(config, mgr_->getGlobalConfig()); -+ EXPECT_EQ(config->getMinPingRequests(), 13); -+ } -+ -+ /// @brief Exercises checkSuspended(). -+ /// -+ /// This is intended to verify that ping checking is suspended and resumed based -+ /// on the DHCP service state, not to verify every place that checkSuspended() -+ /// is called. -+ void testCheckSuspended() { -+ SKIP_IF(notRoot()); -+ -+ // Create manager with thread-pool size of 3, min_echos 1, -+ // reply_timeout 250 milliseconds. -+ ASSERT_NO_THROW_LOG(createMgr(3, 1, 250)); -+ ASSERT_TRUE(mgr_); -+ -+ // Make a default config. -+ PingCheckConfigPtr config(new PingCheckConfig()); -+ -+ // Give the manager a NetworkState instance. -+ NetworkStatePtr network_state(new NetworkState()); -+ mgr_->setNetworkState(network_state); -+ -+ // Verify that ping checking is not suspended. -+ ASSERT_FALSE(mgr_->checkSuspended()); -+ -+ // Start the manager, then pause it. This lets us start pings without -+ // them changing state. -+ ASSERT_NO_THROW_LOG(mgr_->start()); -+ ASSERT_NO_THROW_LOG(mgr_->pause()); -+ -+ // Verfify the ping store is empty. -+ auto store = mgr_->getStore(); -+ ASSERT_TRUE(store); -+ auto pings = store->getAll(); -+ ASSERT_EQ(0, pings->size()); -+ -+ // Make a lease query pair. -+ auto lqp1 = makeLeaseQueryPair(IOAddress("127.0.0.2"), 111); -+ const uint8_t id1[] = { 0x31, 0x32, 0x33, 0x34 }; -+ ClientIdPtr cid1(new ClientId(id1, sizeof(id1))); -+ lqp1.lease_->client_id_ = cid1; -+ -+ // Now let's try to start a ping for the lease-query pair. It should work. -+ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp1.lease_, lqp1.query_, parking_lot_)); -+ -+ // Verify we have an entry in the store. -+ pings = store->getAll(); -+ ASSERT_EQ(1, pings->size()); -+ -+ // Disable the DHCP service. -+ network_state->disableService(NetworkState::USER_COMMAND); -+ -+ // Make a second lease query pair. Different address, different client. -+ auto lqp2 = makeLeaseQueryPair(IOAddress("127.0.0.3"), 333); -+ const uint8_t id2[] = { 0x31, 0x32, 0x33, 0x35 }; -+ ClientIdPtr cid2(new ClientId(id1, sizeof(id2))); -+ lqp2.lease_->client_id_ = cid2; -+ -+ // Try to start a ping. We should not be able to do it. -+ ASSERT_THROW_MSG(mgr_->startPing(lqp2.lease_, lqp2.query_, parking_lot_), -+ InvalidOperation, -+ "PingCheckMgr::startPing() - DHCP service is suspended!"); -+ -+ // Store should be empty, having been flushed by suspension detection. -+ pings = store->getAll(); -+ ASSERT_EQ(0, pings->size()); -+ -+ // Ping checking should report as suspended. -+ ASSERT_TRUE(mgr_->checkSuspended()); -+ -+ // Re-enable the DHCP service. -+ network_state->enableService(NetworkState::USER_COMMAND); -+ -+ // Suspension checking should lift the suspension and we should once again -+ // be able to start a new ping check. -+ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp2.lease_, lqp2.query_, parking_lot_)); -+ -+ // Store should have one check in it. -+ pings = store->getAll(); -+ ASSERT_EQ(1, pings->size()); -+ -+ // Ping checking should report as not suspended. -+ ASSERT_FALSE(mgr_->checkSuspended()); -+ } -+ -+ /// @brief Manager instance. -+ TestablePingCheckMgrPtr mgr_; -+ -+ /// @brief List of lease/query pairs used during the test, in the order -+ /// they were created. -+ LeaseQueryPairs lease_query_pairs_; -+ -+ /// @brief The mutex used to protect internal state. -+ const boost::scoped_ptr<std::mutex> mutex_; -+ -+ /// @brief Marks the start time of a test. -+ TimeStamp test_start_time_; -+ -+ /// @brief Parking lot where the associated query is parked. -+ /// If empty parking is not being employed. -+ ParkingLotHandlePtr parking_lot_; -+ -+ /// @brief Number of queries unparked during a test. -+ size_t unparked_; -+ -+ /// @brief List of leases that were found to be in-use during a test. -+ LeaseQueryPairs declines_; -+ -+ /// @brief List of leases that were found to be free to use during a test. -+ LeaseQueryPairs frees_; -+}; -+ -+TEST_F(PingCheckMgrTest, operationalBasicsST) { -+ testOperationalBasics(0); -+} -+ -+TEST_F(PingCheckMgrTest, operationalBasicsMT) { -+ MultiThreadingTest mt; -+ testOperationalBasics(3); -+} -+ -+TEST_F(PingCheckMgrTest, startPingST) { -+ testStartPing(); -+} -+ -+TEST_F(PingCheckMgrTest, startPingMT) { -+ MultiThreadingTest mt; -+ testStartPing(); -+} -+ -+TEST_F(PingCheckMgrTest, nextToSendST) { -+ testNextToSend(); -+} -+ -+TEST_F(PingCheckMgrTest, nextToSendMT) { -+ MultiThreadingTest mt; -+ testNextToSend(); -+} -+ -+TEST_F(PingCheckMgrTest, setNextExpirationST) { -+ testSetNextExpiration(); -+} -+ -+TEST_F(PingCheckMgrTest, setNextExpirationMT) { -+ MultiThreadingTest mt; -+ testSetNextExpiration(); -+} -+ -+TEST_F(PingCheckMgrTest, sendCompletedST) { -+ testSendCompleted(); -+} -+ -+TEST_F(PingCheckMgrTest, sendCompletedMT) { -+ MultiThreadingTest mt; -+ testSendCompleted(); -+} -+ -+TEST_F(PingCheckMgrTest, replyReceivedForEchoReplyST) { -+ testReplyReceivedForEchoReply(); -+} -+ -+TEST_F(PingCheckMgrTest, replyReceivedForEchoReplyMT) { -+ MultiThreadingTest mt; -+ testReplyReceivedForEchoReply(); -+} -+ -+TEST_F(PingCheckMgrTest, replyReceivedForTargetUnreachableST) { -+ testReplyReceivedForTargetUnreachable(); -+} -+ -+TEST_F(PingCheckMgrTest, replyReceivedForTargetUnreachableMT) { -+ MultiThreadingTest mt; -+ testReplyReceivedForTargetUnreachable(); -+} -+ -+TEST_F(PingCheckMgrTest, expirationProcessingST) { -+ testExpirationProcessing(); -+} -+ -+TEST_F(PingCheckMgrTest, expirationProcessingMT) { -+ MultiThreadingTest mt; -+ testExpirationProcessing(); -+} -+ -+TEST_F(PingCheckMgrTest, multiplePingsWithReplyST) { -+ testMultiplePingsWithReply(); -+} -+ -+TEST_F(PingCheckMgrTest, multiplePingsWithReplyMT) { -+ MultiThreadingTest mt; -+ testMultiplePingsWithReply(); -+} -+ -+TEST_F(PingCheckMgrTest, multiplePingsWithReplyAndPauseST) { -+ testMultiplePingsWithReplyAndPause(); -+} -+ -+TEST_F(PingCheckMgrTest, multiplePingsWithReplyAndPauseMT) { -+ MultiThreadingTest mt; -+ testMultiplePingsWithReplyAndPause(); -+} -+ -+TEST_F(PingCheckMgrTest, sendCompletedSendFailedST) { -+ testSendCompletedSendFailed(); -+} -+ -+TEST_F(PingCheckMgrTest, sendCompletedSendFailedMT) { -+ MultiThreadingTest mt; -+ testSendCompletedSendFailed(); -+} -+ -+TEST_F(PingCheckMgrTest, shouldPingST) { -+ testShouldPingTest(); -+} -+ -+TEST_F(PingCheckMgrTest, shouldPingMT) { -+ MultiThreadingTest mt; -+ testShouldPingTest(); -+} -+ -+TEST_F(PingCheckMgrTest, getScopedConfigST) { -+ testGetScopedConfig(); -+} -+ -+TEST_F(PingCheckMgrTest, getScopedConfigMT) { -+ MultiThreadingTest mt; -+ testGetScopedConfig(); -+} -+ -+TEST_F(PingCheckMgrTest, checkSuspendedST) { -+ testCheckSuspended(); -+} -+ -+TEST_F(PingCheckMgrTest, checkSuspendedMT) { -+ MultiThreadingTest mt; -+ testCheckSuspended(); -+} -+ -+} // end of anonymous namespace -diff --git a/src/hooks/dhcp/ping_check/tests/ping_context_store_unittests.cc b/src/hooks/dhcp/ping_check/tests/ping_context_store_unittests.cc -new file mode 100644 -index 0000000000..3a8854eb0e ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/tests/ping_context_store_unittests.cc -@@ -0,0 +1,467 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+/// @file This file contains tests which exercise the PingContextStore class. -+ -+#include <config.h> -+#include <ping_context_store.h> -+#include <asiolink/io_address.h> -+#include <testutils/gtest_utils.h> -+#include <testutils/multi_threading_utils.h> -+ -+#include <gtest/gtest.h> -+#include <sstream> -+ -+using namespace std; -+using namespace isc; -+using namespace isc::asiolink; -+using namespace isc::dhcp; -+using namespace isc::ping_check; -+using namespace isc::test; -+using namespace std::chrono; -+ -+namespace { -+ -+/// @brief Text fixture class for @c PingContextStore -+/// -+/// In order to facilitate single and multi threaded testing, -+/// individual tests are implemented as methods that are called -+/// from within TEST_F bodies rather than in TEST_F bodies. -+class PingContextStoreTest : public ::testing::Test { -+public: -+ -+ /// @brief Constructor -+ PingContextStoreTest() { -+ } -+ -+ /// @brief Destructor -+ virtual ~PingContextStoreTest() = default; -+ -+ /// @brief Verifies that contexts can be added to the store given valid leases and queries. -+ /// Also verifies that they can be fetched by address. -+ void addContextTest() { -+ PingContextStore store; -+ PingContextPtr context; -+ -+ // Add three contexts, one for each lease/query. -+ auto now = PingContext::now(); -+ for (int i = 0; i < leases_.size(); ++i) { -+ ASSERT_NO_THROW_LOG(context = store.addContext(leases_[i], queries_[i], 2, 300)); -+ ASSERT_TRUE(context); -+ EXPECT_EQ(leases_[i], context->getLease()); -+ EXPECT_EQ(queries_[i], context->getQuery()); -+ -+ // Check initial values. -+ EXPECT_EQ(PingContext::WAITING_TO_SEND, context->getState()); -+ EXPECT_LE(now, context->getSendWaitStart()); -+ EXPECT_EQ(2, context->getMinEchos()); -+ EXPECT_EQ(300, context->getReplyTimeout()); -+ } -+ -+ // Make sure they can be fetched by address and by query individually. -+ for (int i = 0; i < leases_.size(); ++i) { -+ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[i]->addr_)); -+ ASSERT_TRUE(context); -+ EXPECT_EQ(leases_[i], context->getLease()); -+ -+ ASSERT_NO_THROW_LOG(context = store.getContextByQuery(queries_[i])); -+ ASSERT_TRUE(context); -+ EXPECT_EQ(queries_[i], context->getQuery()); -+ } -+ } -+ -+ /// @brief Verifies that the store only allows once entry per IP address. -+ void addContextDuplicateTest() { -+ PingContextStore store; -+ PingContextPtr context; -+ -+ ASSERT_NO_THROW_LOG(context = store.addContext(leases_[0], queries_[0], 1, 100)); -+ ASSERT_TRUE(context); -+ ASSERT_THROW_MSG(store.addContext(leases_[0], queries_[0], 1, 100), DuplicateContext, -+ "PingContextStore::addContex: context already exists for: 192.0.2.1"); -+ } -+ -+ /// @brief Verify that addContext fails given invalid input. -+ void addContextInvalidTest() { -+ PingContextStore store; -+ -+ // Verify that given an empty lease the add will fail. -+ Lease4Ptr empty_lease; -+ ASSERT_THROW_MSG(store.addContext(empty_lease, queries_[0], 1, 100), BadValue, -+ "PingContextStore::addContext failed:" -+ " PingContext ctor - lease cannot be empty"); -+ -+ // Verify that given an empty query the add will fail. -+ Pkt4Ptr empty_query; -+ ASSERT_THROW_MSG(store.addContext(leases_[0], empty_query, 1, 100), BadValue, -+ "PingContextStore::addContext failed:" -+ " PingContext ctor - query cannot be empty"); -+ } -+ -+ /// @brief Verify that contexts can be deleted from the store. -+ void deleteContextTest() { -+ PingContextStore store; -+ -+ // Add contexts to store. -+ for (int i = 0; i < leases_.size(); ++i) { -+ PingContextPtr context; -+ ASSERT_NO_THROW_LOG(context = store.addContext(leases_[i], queries_[i], 1, 100)); -+ ASSERT_TRUE(context); -+ EXPECT_EQ(leases_[i], context->getLease()); -+ EXPECT_EQ(queries_[i], context->getQuery()); -+ } -+ -+ // Fetch the second context. -+ PingContextPtr orig_context; -+ ASSERT_NO_THROW_LOG(orig_context = store.getContextByAddress(leases_[1]->addr_)); -+ ASSERT_TRUE(orig_context); -+ EXPECT_EQ(leases_[1], orig_context->getLease()); -+ -+ // Delete it. -+ ASSERT_NO_THROW_LOG(store.deleteContext(orig_context)); -+ -+ // Try to fetch it, shouldn't find it. -+ PingContextPtr context; -+ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[1]->addr_)); -+ ASSERT_FALSE(context); -+ -+ // Deleting it again should do no harm. -+ ASSERT_NO_THROW_LOG(store.deleteContext(orig_context)); -+ } -+ -+ /// @brief Verify that contexts in the store can be updated. -+ void updateContextTest() { -+ PingContextStore store; -+ PingContextPtr context; -+ -+ // Try to update a context that doesn't exist. It should throw. -+ ASSERT_NO_THROW_LOG(context.reset(new PingContext(leases_[0], queries_[0]))); -+ ASSERT_THROW_MSG(store.updateContext(context), InvalidOperation, -+ "PingContextStore::updateContext failed for address:" -+ " 192.0.2.1, not in store"); -+ -+ auto test_start = PingContext::now(); -+ -+ // Add contexts to store. -+ for (int i = 0; i < leases_.size(); ++i) { -+ ASSERT_NO_THROW_LOG(context = store.addContext(leases_[i], queries_[i], 1, 100)); -+ ASSERT_TRUE(context); -+ EXPECT_EQ(leases_[i], context->getLease()); -+ EXPECT_EQ(queries_[i], context->getQuery()); -+ } -+ -+ // Fetch the second context. -+ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[1]->addr_)); -+ ASSERT_TRUE(context); -+ ASSERT_EQ(leases_[1], context->getLease()); -+ ASSERT_EQ(queries_[1], context->getQuery()); -+ -+ // Check initial values for state and expiration. -+ EXPECT_EQ(PingContext::WAITING_TO_SEND, context->getState()); -+ EXPECT_LE(test_start, context->getSendWaitStart()); -+ EXPECT_LE(PingContext::EMPTY_TIME(), context->getNextExpiry()); -+ -+ // Modify the state and expiration, then update the context. -+ auto wait_start = PingContext::now(); -+ context->beginWaitingForReply(wait_start); -+ ASSERT_NO_THROW_LOG(store.updateContext(context)); -+ -+ // Fetch the context and verify the values are correct. -+ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[1]->addr_)); -+ ASSERT_TRUE(context); -+ EXPECT_EQ(PingContext::WAITING_FOR_REPLY, context->getState()); -+ EXPECT_LE(wait_start + milliseconds(context->getReplyTimeout()), context->getNextExpiry()); -+ } -+ -+ /// @brief Verify that contexts can be fetched based on when they entered WAITING_TO_SEND -+ /// by getNextToSend(). -+ void getNextToSendTest() { -+ PingContextStore store; -+ PingContextPtr context; -+ -+ // Capture time now. -+ auto start_time = PingContext::now(); -+ -+ // Add contexts to store. -+ for (int i = 0; i < leases_.size(); ++i) { -+ ASSERT_NO_THROW_LOG(context = store.addContext(leases_[i], queries_[i], 1, 100)); -+ ASSERT_TRUE(context); -+ EXPECT_EQ(leases_[i], context->getLease()); -+ EXPECT_EQ(queries_[i], context->getQuery()); -+ usleep(1000); -+ } -+ -+ // Fetching the next context to send should return the first context as -+ // it has the oldest send wait start time. -+ context.reset(); -+ ASSERT_NO_THROW(context = store.getNextToSend()); -+ ASSERT_TRUE(context); -+ EXPECT_EQ(leases_[0], context->getLease()); -+ EXPECT_EQ(queries_[0], context->getQuery()); -+ EXPECT_LE(start_time, context->getSendWaitStart()); -+ -+ // Update the first context's state to TARGET_FREE which should -+ // disqualify it from being returned as next to send. -+ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[0]->addr_)); -+ ASSERT_TRUE(context); -+ ASSERT_EQ(PingContext::WAITING_TO_SEND, context->getState()); -+ context->setState(PingContext::TARGET_FREE); -+ ASSERT_NO_THROW_LOG(store.updateContext(context)); -+ -+ // Update the send wait start of the second context making it the -+ // youngest send wait start time. -+ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[1]->addr_)); -+ ASSERT_TRUE(context); -+ ASSERT_EQ(PingContext::WAITING_TO_SEND, context->getState()); -+ context->setSendWaitStart(start_time + milliseconds(1000)); -+ ASSERT_NO_THROW_LOG(store.updateContext(context)); -+ -+ // Update the send wait start of the third context, making it the oldest. -+ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[2]->addr_)); -+ ASSERT_TRUE(context); -+ ASSERT_EQ(PingContext::WAITING_TO_SEND, context->getState()); -+ context->setSendWaitStart(start_time + milliseconds(500)); -+ ASSERT_NO_THROW_LOG(store.updateContext(context)); -+ -+ // Fetching the next context to send should return the third context. -+ context.reset(); -+ ASSERT_NO_THROW(context = store.getNextToSend()); -+ ASSERT_TRUE(context); -+ EXPECT_EQ(leases_[2], context->getLease()); -+ EXPECT_EQ(queries_[2], context->getQuery()); -+ EXPECT_EQ(start_time + milliseconds(500), context->getSendWaitStart()); -+ } -+ -+ /// @brief Verify that contexts can be fetched based on when they expire using -+ /// getExpiresNext() and getExpiredSince(). -+ void getByExpirationTest() { -+ PingContextStore store; -+ PingContextPtr context; -+ -+ // Add contexts to store. -+ for (int i = 0; i < leases_.size(); ++i) { -+ ASSERT_NO_THROW_LOG(context = store.addContext(leases_[i], queries_[i], 1, 100)); -+ ASSERT_TRUE(context); -+ EXPECT_EQ(leases_[i], context->getLease()); -+ EXPECT_EQ(queries_[i], context->getQuery()); -+ } -+ -+ // Capture time now. -+ auto start_time = PingContext::now(); -+ -+ // Update the state and expiration of the first context. -+ // State set to TARGET_FREE should disqualify if from -+ // fetch by expiration even though it has the soonest expiration -+ // time. -+ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[0]->addr_)); -+ ASSERT_TRUE(context); -+ context->setState(PingContext::TARGET_FREE); -+ context->setNextExpiry(start_time + milliseconds(1)); -+ ASSERT_NO_THROW_LOG(store.updateContext(context)); -+ -+ // Update the state and expiration of the second context giving it -+ // the youngest expiration time. -+ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[1]->addr_)); -+ ASSERT_TRUE(context); -+ context->setState(PingContext::WAITING_FOR_REPLY); -+ context->setNextExpiry(start_time + milliseconds(1000)); -+ ASSERT_NO_THROW_LOG(store.updateContext(context)); -+ -+ // Update the state and expiration of the third context, make it the -+ // soonest qualified expiration time. -+ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[2]->addr_)); -+ ASSERT_TRUE(context); -+ context->setState(PingContext::WAITING_FOR_REPLY); -+ context->setNextExpiry(start_time + milliseconds(500)); -+ ASSERT_NO_THROW_LOG(store.updateContext(context)); -+ -+ // Fetching the context that expires next should return the third context. -+ context.reset(); -+ ASSERT_NO_THROW(context = store.getExpiresNext()); -+ ASSERT_TRUE(context); -+ EXPECT_EQ(leases_[2], context->getLease()); -+ EXPECT_EQ(queries_[2], context->getQuery()); -+ EXPECT_EQ(start_time + milliseconds(500), context->getNextExpiry()); -+ -+ // Fetch all that have expired since current time. Should be none. -+ PingContextCollectionPtr expired_since; -+ ASSERT_NO_THROW_LOG(expired_since = store.getExpiredSince()); -+ ASSERT_TRUE(expired_since); -+ EXPECT_EQ(0, expired_since->size()); -+ -+ // Fetch all that have expired since start time + 750 ms, should be third context. -+ ASSERT_NO_THROW_LOG(expired_since = store.getExpiredSince(start_time + milliseconds(750))); -+ ASSERT_TRUE(expired_since); -+ EXPECT_EQ(1, expired_since->size()); -+ context = (*expired_since)[0]; -+ EXPECT_EQ(leases_[2], context->getLease()); -+ EXPECT_EQ(queries_[2], context->getQuery()); -+ EXPECT_EQ(start_time + milliseconds(500), context->getNextExpiry()); -+ -+ // Fetch all that have expired since start time + 1500 ms -+ // Should be the third and second contexts -+ ASSERT_NO_THROW_LOG(expired_since = store.getExpiredSince(start_time + milliseconds(1500))); -+ ASSERT_TRUE(expired_since); -+ EXPECT_EQ(2, expired_since->size()); -+ -+ // First in list should be the third context. -+ context = (*expired_since)[0]; -+ EXPECT_EQ(leases_[2], context->getLease()); -+ EXPECT_EQ(queries_[2], context->getQuery()); -+ EXPECT_EQ(start_time + milliseconds(500), context->getNextExpiry()); -+ -+ // The last one in the list should be the second context. -+ context = (*expired_since)[1]; -+ EXPECT_EQ(leases_[1], context->getLease()); -+ EXPECT_EQ(queries_[1], context->getQuery()); -+ EXPECT_EQ(start_time + milliseconds(1000), context->getNextExpiry()); -+ } -+ -+ /// @brief Verifies that getAll() and clear() work properly. -+ void getAllAndClearTest() { -+ PingContextStore store; -+ -+ // Add contexts to store. -+ for (int i = 0; i < leases_.size(); ++i) { -+ PingContextPtr context; -+ ASSERT_NO_THROW_LOG(context = store.addContext(leases_[i], queries_[i], 1, 100)); -+ ASSERT_TRUE(context); -+ EXPECT_EQ(leases_[i], context->getLease()); -+ EXPECT_EQ(queries_[i], context->getQuery()); -+ } -+ -+ // Fetch them all. -+ PingContextCollectionPtr contexts; -+ ASSERT_NO_THROW_LOG(contexts = store.getAll()); -+ ASSERT_EQ(leases_.size(), contexts->size()); -+ -+ // Verify we got them all in order. -+ int i = 0; -+ for (auto const& context : *contexts) { -+ EXPECT_EQ(leases_[i], context->getLease()); -+ EXPECT_EQ(queries_[i], context->getQuery()); -+ ++i; -+ } -+ -+ // Now clear the store. Verify it's empty. -+ ASSERT_NO_THROW_LOG(store.clear()); -+ ASSERT_NO_THROW_LOG(contexts = store.getAll()); -+ ASSERT_EQ(0, contexts->size()); -+ -+ // Verify clearing an empty store does no harm. -+ ASSERT_NO_THROW_LOG(store.clear()); -+ } -+ -+private: -+ /// @brief Prepares the class for a test. -+ virtual void SetUp() { -+ Lease4Ptr lease; -+ lease.reset(new Lease4()); -+ lease->addr_ = IOAddress("192.0.2.1"); -+ leases_.push_back(lease); -+ -+ lease.reset(new Lease4()); -+ lease->addr_ = IOAddress("192.0.2.2"); -+ leases_.push_back(lease); -+ -+ lease.reset(new Lease4()); -+ lease->addr_ = IOAddress("192.0.2.3"); -+ leases_.push_back(lease); -+ -+ Pkt4Ptr query; -+ query.reset(new Pkt4(DHCPDISCOVER, 101)); -+ queries_.push_back(query); -+ -+ query.reset(new Pkt4(DHCPDISCOVER, 102)); -+ queries_.push_back(query); -+ -+ query.reset(new Pkt4(DHCPDISCOVER, 103)); -+ queries_.push_back(query); -+ -+ ASSERT_EQ(leases_.size(), queries_.size()); -+ } -+ -+public: -+ /// @brief List of pre-made leases. -+ std::vector<Lease4Ptr> leases_; -+ -+ /// @brief List of pre-made queries. -+ std::vector<Pkt4Ptr> queries_; -+}; -+ -+TEST_F(PingContextStoreTest, addContext) { -+ addContextTest(); -+} -+ -+TEST_F(PingContextStoreTest, addContextMultiThreading) { -+ MultiThreadingTest mt; -+ addContextTest(); -+} -+ -+TEST_F(PingContextStoreTest, addContextDuplicate) { -+ addContextDuplicateTest(); -+} -+ -+TEST_F(PingContextStoreTest, addContextDuplicateMultiThreading) { -+ MultiThreadingTest mt; -+ addContextDuplicateTest(); -+} -+ -+TEST_F(PingContextStoreTest, addContextInvalid) { -+ addContextInvalidTest(); -+} -+ -+TEST_F(PingContextStoreTest, addContextInvalidMultiThreading) { -+ MultiThreadingTest mt; -+ addContextInvalidTest(); -+} -+ -+TEST_F(PingContextStoreTest, deleteContext) { -+ deleteContextTest(); -+} -+ -+TEST_F(PingContextStoreTest, deleteContextMultiThreading) { -+ MultiThreadingTest mt; -+ deleteContextTest(); -+} -+ -+TEST_F(PingContextStoreTest, updateContext) { -+ updateContextTest(); -+} -+ -+TEST_F(PingContextStoreTest, updateContextMultiThreading) { -+ MultiThreadingTest mt; -+ updateContextTest(); -+} -+ -+TEST_F(PingContextStoreTest, getNextToSend) { -+ getNextToSendTest(); -+} -+ -+TEST_F(PingContextStoreTest, getNextToSendMultiThreading) { -+ MultiThreadingTest mt; -+ getNextToSendTest(); -+} -+ -+TEST_F(PingContextStoreTest, getByExpiration) { -+ getByExpirationTest(); -+} -+ -+TEST_F(PingContextStoreTest, getByExpirationMultiThreading) { -+ MultiThreadingTest mt; -+ getByExpirationTest(); -+} -+ -+TEST_F(PingContextStoreTest, getAllAndClear) { -+ getAllAndClearTest(); -+} -+ -+TEST_F(PingContextStoreTest, getAllAndClearMultiThreading) { -+ MultiThreadingTest mt; -+ getAllAndClearTest(); -+} -+ -+} // end of anonymous namespace -diff --git a/src/hooks/dhcp/ping_check/tests/ping_context_unittests.cc b/src/hooks/dhcp/ping_check/tests/ping_context_unittests.cc -new file mode 100644 -index 0000000000..4a38277ad6 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/tests/ping_context_unittests.cc -@@ -0,0 +1,146 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+/// @file This file contains tests which exercise the PingContext class. -+ -+#include <config.h> -+#include <ping_context.h> -+#include <asiolink/io_address.h> -+#include <testutils/gtest_utils.h> -+ -+#include <gtest/gtest.h> -+#include <sstream> -+ -+using namespace std; -+using namespace isc; -+using namespace isc::asiolink; -+using namespace isc::dhcp; -+using namespace isc::ping_check; -+using namespace std::chrono; -+ -+namespace { -+ -+TEST(PingContextTest, validConstruction) { -+ // Make a valid lease and query. -+ Lease4Ptr lease(new Lease4()); -+ lease->addr_ = IOAddress("192.0.2.1"); -+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234)); -+ -+ // Capture time now. -+ auto start_time = PingContext::now(); -+ -+ // Construct the context. -+ PingContextPtr context; -+ ASSERT_NO_THROW_LOG(context.reset(new PingContext(lease, query))); -+ -+ // Verify initial content. -+ EXPECT_EQ(lease->addr_, context->getTarget()); -+ EXPECT_EQ(1, context->getMinEchos()); -+ EXPECT_EQ(100, context->getReplyTimeout()); -+ EXPECT_EQ(0, context->getEchosSent()); -+ EXPECT_EQ(PingContext::EMPTY_TIME(), context->getLastEchoSentTime()); -+ EXPECT_EQ(PingContext::EMPTY_TIME(), context->getSendWaitStart()); -+ EXPECT_EQ(PingContext::EMPTY_TIME(), context->getNextExpiry()); -+ EXPECT_EQ(PingContext::NEW, context->getState()); -+ -+ // Start time should be less than or equal to created time. -+ EXPECT_LE(start_time, context->getCreatedTime()); -+ EXPECT_EQ(lease, context->getLease()); -+ EXPECT_EQ(query, context->getQuery()); -+} -+ -+TEST(PingContextTest, invalidConstruction) { -+ // Make a valid lease and query. -+ Lease4Ptr lease(new Lease4()); -+ lease->addr_ = IOAddress("192.0.2.1"); -+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234)); -+ -+ // Empty lease should throw. -+ Lease4Ptr empty_lease; -+ PingContextPtr context; -+ ASSERT_THROW_MSG(context.reset(new PingContext(empty_lease, query)), BadValue, -+ "PingContext ctor - lease cannot be empty"); -+ -+ // Empty query should throw. -+ Pkt4Ptr empty_query; -+ ASSERT_THROW_MSG(context.reset(new PingContext(lease, empty_query)), BadValue, -+ "PingContext ctor - query cannot be empty"); -+ -+ // Empty lease address should throw. -+ lease->addr_ = IOAddress::IPV4_ZERO_ADDRESS(); -+ ASSERT_THROW_MSG(context.reset(new PingContext(lease, query)), BadValue, -+ "PingContext ctor - target address cannot be 0.0.0.0"); -+} -+ -+// Tests conversion of PingContext::State to string and vice-versa. -+TEST(PingContext, stateConversion) { -+ EXPECT_EQ(PingContext::NEW, PingContext::stringToState("NEW")); -+ EXPECT_EQ(PingContext::WAITING_TO_SEND, PingContext::stringToState("WAITING_TO_SEND")); -+ EXPECT_EQ(PingContext::SENDING, PingContext::stringToState("SENDING")); -+ EXPECT_EQ(PingContext::WAITING_FOR_REPLY, PingContext::stringToState("WAITING_FOR_REPLY")); -+ EXPECT_EQ(PingContext::TARGET_FREE, PingContext::stringToState("TARGET_FREE")); -+ EXPECT_EQ(PingContext::TARGET_IN_USE, PingContext::stringToState("TARGET_IN_USE")); -+ ASSERT_THROW_MSG(PingContext::stringToState("bogus"), BadValue, -+ "Invalid PingContext::State: 'bogus'"); -+ -+ EXPECT_EQ("NEW", PingContext::stateToString(PingContext::NEW)); -+ EXPECT_EQ("WAITING_TO_SEND", PingContext::stateToString(PingContext::WAITING_TO_SEND)); -+ EXPECT_EQ("SENDING", PingContext::stateToString(PingContext::SENDING)); -+ EXPECT_EQ("WAITING_FOR_REPLY", PingContext::stateToString(PingContext::WAITING_FOR_REPLY)); -+ EXPECT_EQ("TARGET_FREE", PingContext::stateToString(PingContext::TARGET_FREE)); -+ EXPECT_EQ("TARGET_IN_USE", PingContext::stateToString(PingContext::TARGET_IN_USE)); -+} -+ -+TEST(PingContext, accessors) { -+ // Make a valid lease and query. -+ Lease4Ptr lease(new Lease4()); -+ lease->addr_ = IOAddress("192.0.2.1"); -+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234)); -+ -+ // Capture time now. -+ auto time_now = PingContext::now(); -+ -+ // Construct a context. -+ PingContextPtr context; -+ ASSERT_NO_THROW_LOG(context.reset(new PingContext(lease, query, 1, 50))); -+ -+ EXPECT_NO_THROW_LOG(context->setMinEchos(4)); -+ EXPECT_EQ(4, context->getMinEchos()); -+ -+ EXPECT_NO_THROW_LOG(context->setReplyTimeout(200)); -+ EXPECT_EQ(200, context->getReplyTimeout()); -+ -+ EXPECT_NO_THROW_LOG(context->setEchosSent(7)); -+ EXPECT_EQ(7, context->getEchosSent()); -+ -+ EXPECT_NO_THROW_LOG(context->setLastEchoSentTime(time_now)); -+ EXPECT_EQ(time_now, context->getLastEchoSentTime()); -+ -+ EXPECT_NO_THROW_LOG(context->setState(PingContext::SENDING)); -+ EXPECT_EQ(PingContext::SENDING, context->getState()); -+ -+ time_now += milliseconds(100); -+ EXPECT_NO_THROW_LOG(context->setSendWaitStart(time_now)); -+ EXPECT_EQ(time_now, context->getSendWaitStart()); -+ -+ time_now += milliseconds(100); -+ EXPECT_NO_THROW_LOG(context->setNextExpiry(time_now)); -+ EXPECT_EQ(time_now, context->getNextExpiry()); -+ -+ EXPECT_FALSE(context->isWaitingToSend()); -+ time_now += milliseconds(100); -+ ASSERT_NO_THROW_LOG(context->beginWaitingToSend(time_now)); -+ EXPECT_EQ(time_now, context->getSendWaitStart()); -+ EXPECT_TRUE(context->isWaitingToSend()); -+ -+ EXPECT_FALSE(context->isWaitingForReply()); -+ auto exp_expiry = time_now + milliseconds(context->getReplyTimeout()); -+ ASSERT_NO_THROW_LOG(context->beginWaitingForReply(time_now)); -+ EXPECT_EQ(exp_expiry, context->getNextExpiry()); -+ EXPECT_TRUE(context->isWaitingForReply()); -+} -+ -+} // end of anonymous namespace -diff --git a/src/hooks/dhcp/ping_check/tests/ping_test_utils.h b/src/hooks/dhcp/ping_check/tests/ping_test_utils.h -new file mode 100644 -index 0000000000..df1ede7526 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/tests/ping_test_utils.h -@@ -0,0 +1,396 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#ifndef PING_TEST_UTILS_H -+#define PING_TEST_UTILS_H -+ -+#include <ping_channel.h> -+#include <asiolink/interval_timer.h> -+#include <asiolink/io_service.h> -+#include <asiolink/io_address.h> -+#include <testutils/gtest_utils.h> -+#include <asiolink/io_service_thread_pool.h> -+#include <util/multi_threading_mgr.h> -+#include <mutex> -+ -+#include <gtest/gtest.h> -+#include <queue> -+#include <list> -+#include <thread> -+#include <map> -+ -+namespace isc { -+namespace ping_check { -+ -+/// @brief Test timeout (ms). -+const long TEST_TIMEOUT = 10000; -+ -+/// @brief Maps IOAddresses to sequence numbers. -+/// -+/// Outbound requests are assigned a unique id and sequence -+/// number. This map is used to track the request's destination -+/// address by its sequence number. The channel can then substitute -+/// the loopback address, 127.0.0.1, as the destination address. -+/// Upon response receipt, the original destination can be found by -+/// the sequence number sent back in the response. -+class LoopbackMap { -+public: -+ /// @brief Constructor. -+ LoopbackMap() : map_(), mutex_(new std::mutex) { -+ } -+ -+ /// @brief Destructor. -+ ~LoopbackMap() = default; -+ -+ /// @brief Find and IOAddress associated with a sequence number. -+ /// -+ /// @param sequence sequence number to search by -+ /// -+ /// @return address found or IPV4_ZERO_ADDRESS. -+ asiolink::IOAddress find(uint16_t sequence) { -+ util::MultiThreadingLock lock(*mutex_); -+ auto const& iter = map_.find(sequence); -+ if (iter == map_.end()) { -+ return (asiolink::IOAddress::IPV4_ZERO_ADDRESS()); -+ } -+ -+ return (iter->second); -+ } -+ -+ /// @brief Adds an entry for a sequence number and address -+ /// -+ /// @param sequence sequence number associated with the address -+ /// @param address address to add to the map -+ /// -+ /// @return true if the entry was added, false otherwise. -+ bool add(uint16_t sequence, const asiolink::IOAddress& address) { -+ util::MultiThreadingLock lock(*mutex_); -+ if (map_.count(sequence)) { -+ return (false); -+ } -+ -+ map_.emplace(sequence, address); -+ return (true); -+ }; -+ -+ /// @brief Map of addresses by sequence number. -+ std::map<uint16_t, asiolink::IOAddress> map_; -+ -+ /// @brief Mutex to protect the map during operations. -+ const boost::scoped_ptr<std::mutex> mutex_; -+}; -+ -+/// @brief Testable derivation of PingChannel -+/// -+/// Overrides read and write functions to inject IO errors. -+class TestablePingChannel : public PingChannel { -+public: -+ /// @brief Constructor -+ /// -+ /// Instantiates the channel with its socket closed. -+ /// -+ /// @param io_service pointer to the IOService instance that will manage -+ /// the channel's IO. Must not be empty -+ /// @param next_to_send_cb callback to invoke to fetch the next IOAddress -+ /// to ping -+ /// @param echo_sent_cb callback to invoke when an ECHO send has completed -+ /// @param reply_received_cb callback to invoke when an ICMP reply has been -+ /// received. This callback is passed all inbound ICMP messages (e.g. ECHO -+ /// REPLY, UNREACHABLE, etc...) -+ /// @param shutdown_cb callback to invoke when the channel has shutdown due -+ /// to an error -+ /// -+ /// @throw BadValue if io_service is empty. -+ TestablePingChannel(asiolink::IOServicePtr& io_service, -+ NextToSendCallback next_to_send_cb, -+ EchoSentCallback echo_sent_cb, -+ ReplyReceivedCallback reply_received_cb, -+ ShutdownCallback shutdown_cb = ShutdownCallback()) -+ : PingChannel(io_service, next_to_send_cb, echo_sent_cb, reply_received_cb, shutdown_cb), -+ read_number_(0), throw_on_read_number_(0), ec_on_read_number_(0), read_error_ec_(), -+ write_number_(0), throw_on_write_number_(0), ec_on_write_number_(0), write_error_ec_(), -+ route_loopback_(true), loopback_map_(), stopped_(false) { -+ } -+ -+ /// @brief Virtual destructor -+ virtual ~TestablePingChannel() { -+ stopped_ = true; -+ } -+ -+ // @brief Schedules the next send. -+ // -+ // If the socket is not currently sending it posts a call to @c sendNext() -+ // to the channel's IOService. -+ virtual void startSend() { -+ if (stopped_) { -+ return; -+ } -+ PingChannel::startSend(); -+ } -+ -+ /// @brief Perform asynchronous read or feign a read error -+ /// -+ /// This virtual function is provided as means to inject errors during -+ /// read operations to facilitate testing. It tracks the number of -+ /// reads that have occurred since channel open and instigates an -+ /// error trigger on the trigger read number if a trigger has been set. -+ /// -+ /// @param data buffer to receive incoming message -+ /// @param length length of the data buffer -+ /// @param offset offset into buffer where data is to be put -+ /// @param endpoint source of the communication -+ /// @param callback callback object -+ virtual void asyncReceive(void* data, size_t length, size_t offset, -+ asiolink::IOEndpoint* endpoint, SocketCallback& callback) { -+ if (stopped_) { -+ return; -+ } -+ ++read_number_; -+ -+ // If we're set to fail with an exception, do so. -+ if (throw_on_read_number_ && (read_number_ == throw_on_read_number_)) { -+ isc_throw(Unexpected, "Injected read error"); -+ } -+ -+ // If we're set to fail via the callback, post a call with the -+ // desired error code. -+ if (ec_on_read_number_ && read_number_ == ec_on_read_number_) { -+ getIOService()->post([this]() { socketReadCallback(read_error_ec_, 0); }); -+ return; -+ } -+ -+ // No scheduled error, proceed with normal read. -+ PingChannel::asyncReceive(data, length, offset, endpoint, callback); -+ } -+ -+ /// @brief Perform asynchronous write or feign a write error -+ /// -+ /// This virtual function is provided as means to inject errors during -+ /// write operations to facilitate testing. It tracks the number of -+ /// writes that have occurred since channel open and instigates an -+ /// error trigger on the trigger write number if a trigger has been set. -+ /// -+ /// @param data buffer of data to write -+ /// @param length length of the data buffer -+ /// @param endpoint destination of the communication -+ /// @param callback callback object -+ virtual void asyncSend(void* data, size_t length, asiolink::IOEndpoint* endpoint, -+ SocketCallback& callback) { -+ if (stopped_) { -+ return; -+ } -+ ++write_number_; -+ if (throw_on_write_number_ && (write_number_ == throw_on_write_number_)) { -+ isc_throw(Unexpected, "Injected write error"); -+ } -+ -+ if (ec_on_write_number_ && write_number_ == ec_on_write_number_) { -+ ICMPMsgPtr fake_echo(new ICMPMsg()); -+ fake_echo->setType(ICMPMsg::ECHO_REQUEST); -+ fake_echo->setDestination(endpoint->getAddress()); -+ getIOService()->post([this, fake_echo]() { socketWriteCallback(fake_echo, write_error_ec_, 0); }); -+ return; -+ } -+ -+ // In order to make testing more predictable, we need slow writes down a bit. -+ usleep(5000); -+ -+ // If loopback routing is enabled, store the destination address by -+ // sequence number in the loopback map, then replace the destination -+ // endpoint with 127.0.0.1 and send it there. -+ if (route_loopback_) { -+ struct icmp* reply = (struct icmp*)(data); -+ auto sequence = (ntohs(reply->icmp_hun.ih_idseq.icd_seq)); -+ loopback_map_.add(sequence, endpoint->getAddress()); -+ ICMPEndpoint lo_endpoint(asiolink::IOAddress("127.0.0.1")); -+ PingChannel::asyncSend(data, length, &lo_endpoint, callback); -+ return; -+ } -+ -+ PingChannel::asyncSend(data, length, endpoint, callback); -+ } -+ -+ /// @brief Fetches the PingSocket. -+ /// -+ /// @return pointer to the PingSocket instance. -+ PingSocketPtr getPingSocket() { -+ return (socket_); -+ } -+ -+ /// @brief Checks if channel was opened in single-threaded mode. -+ /// -+ /// @return True if channel is single-threaded. -+ bool getSingleThreaded() const { -+ return (single_threaded_); -+ } -+ -+ /// @brief Fetch the WatchSocket instance. -+ /// -+ /// @return pointer to the WatchSocket. -+ util::WatchSocketPtr getWatchSocket() const { -+ return (watch_socket_); -+ } -+ -+ /// @brief The "write-ready" socket descriptor registered IfaceMgr. -+ /// -+ /// @return registered socket descriptor. -+ int getRegisteredWriteFd() const { -+ return (registered_write_fd_); -+ } -+ -+ /// @brief The "read-ready" socket descriptor registered IfaceMgr. -+ /// -+ /// @return registered socket descriptor. -+ int getRegisteredReadFd() const { -+ return (registered_read_fd_); -+ } -+ -+ /// @brief Tracks the number of reads since the channel was created -+ size_t read_number_; -+ -+ /// @brief Read number on which to thrown an exception from asyncReceive() -+ size_t throw_on_read_number_; -+ -+ /// @brief Read number on which to inject a socketReadCallback with an error code -+ size_t ec_on_read_number_; -+ -+ /// @brief Error code to inject on read error trigger -+ boost::system::error_code read_error_ec_; -+ -+ /// @brief Tracks the number of writes since the channel was created -+ size_t write_number_; -+ -+ /// @brief Write number on which to thrown an exception from asyncSend() -+ size_t throw_on_write_number_; -+ -+ /// @brief Error code to inject on write error trigger -+ size_t ec_on_write_number_; -+ -+ /// @brief Error code to inject on write error trigger -+ boost::system::error_code write_error_ec_; -+ -+ /// @brief Enables routing of 127.0.0.x by to 127.0.0.1 via sequence number. -+ bool route_loopback_; -+ -+ /// @brief Maps loopback addresses to sequence numbers when loopback routing -+ /// is enabled. -+ LoopbackMap loopback_map_; -+ -+ /// @brief Flag which indicates that the manager has been stopped. -+ bool stopped_; -+}; -+ -+/// @brief Defines a pointer to a TestablePingChannel -+typedef boost::shared_ptr<TestablePingChannel> TestablePingChannelPtr; -+ -+/// @brief Defines a callback type for test completion check functions. -+typedef std::function<bool()> TestDoneCallback; -+ -+/// @brief Test fixture class which uses an IOService for time management and/or IO -+class IOServiceTest : public ::testing::Test { -+public: -+ /// @brief Constructor. -+ /// -+ /// Starts test timer which detects timeouts. -+ IOServiceTest() -+ : test_io_service_(new asiolink::IOService()), -+ test_timer_(test_io_service_), -+ run_io_service_timer_(test_io_service_), -+ test_done_cb_() { -+ test_timer_.setup(std::bind(&IOServiceTest::timeoutHandler, this, true), -+ TEST_TIMEOUT, -+ asiolink::IntervalTimer::ONE_SHOT); -+ } -+ -+ /// @brief Indicates if current user is not root -+ /// -+ /// @return True if neither the uid or the effective -+ /// uid is root. -+ static bool notRoot() { -+ return (getuid() != 0 && geteuid() != 0); -+ } -+ -+ /// @brief Destructor. -+ /// -+ /// Removes active clients. -+ virtual ~IOServiceTest() { -+ test_timer_.cancel(); -+ run_io_service_timer_.cancel(); -+ test_io_service_->stopAndPoll(); -+ } -+ -+ /// @brief Callback function invoke upon test timeout. -+ /// -+ /// It stops the IO service and reports test timeout. -+ /// -+ /// @param fail_on_timeout Specifies if test failure should be reported. -+ void timeoutHandler(const bool fail_on_timeout) { -+ if (fail_on_timeout) { -+ ADD_FAILURE() << "Timeout occurred while running the test!"; -+ } -+ -+ test_io_service_->stop(); -+ } -+ -+ /// @brief Stops the IOService if criteria for test completion has been met. -+ /// -+ /// Stops the IOService If there either no test completion callback or the -+ /// call back returns true. -+ void stopIfDone() { -+ // If there is no done test callback or it returns true, stop the service. -+ if (!test_done_cb_ || (test_done_cb_)()) { -+ test_io_service_->stop(); -+ } -+ } -+ -+ /// @brief Posts a call to stop the io service to the io service. -+ /// -+ /// This should be used when stopping the service from callbacks on -+ /// thread pool threads. -+ void stopTestService() { -+ if (!test_io_service_->stopped()) { -+ test_io_service_->post([&]() { test_io_service_->stop(); }); -+ } -+ } -+ -+ /// @brief Runs IO service with optional timeout. -+ /// -+ /// @param timeout number of milliseconds to run the io service. Defaults to -+ /// zero which means run forever. -+ void runIOService(long timeout = 0) { -+ test_io_service_->stop(); -+ test_io_service_->restart(); -+ -+ if (timeout > 0) { -+ run_io_service_timer_.setup(std::bind(&IOServiceTest::timeoutHandler, -+ this, false), -+ timeout, -+ asiolink::IntervalTimer::ONE_SHOT); -+ } -+ -+ test_io_service_->run(); -+ test_io_service_->stopAndPoll(); -+ } -+ -+ /// @brief IO service used in the tests. -+ asiolink::IOServicePtr test_io_service_; -+ -+ /// @brief Asynchronous timer service to detect timeouts. -+ asiolink::IntervalTimer test_timer_; -+ -+ /// @brief Asynchronous timer for running IO service for a specified amount -+ /// of time. -+ asiolink::IntervalTimer run_io_service_timer_; -+ -+ /// @brief Callback function which event handlers can use to check if service -+ /// run should stop. -+ TestDoneCallback test_done_cb_; -+}; -+ -+} // end of namespace ping_check -+} // end of namespace isc -+ -+#endif -diff --git a/src/hooks/dhcp/ping_check/tests/run_unittests.cc b/src/hooks/dhcp/ping_check/tests/run_unittests.cc -new file mode 100644 -index 0000000000..d249e2362e ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/tests/run_unittests.cc -@@ -0,0 +1,19 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#include <config.h> -+ -+#include <log/logger_support.h> -+#include <gtest/gtest.h> -+ -+int -+main(int argc, char* argv[]) { -+ ::testing::InitGoogleTest(&argc, argv); -+ isc::log::initLogger(); -+ int result = RUN_ALL_TESTS(); -+ -+ return (result); -+} -diff --git a/src/hooks/dhcp/ping_check/version.cc b/src/hooks/dhcp/ping_check/version.cc -new file mode 100644 -index 0000000000..f2250ab126 ---- /dev/null -+++ b/src/hooks/dhcp/ping_check/version.cc -@@ -0,0 +1,17 @@ -+// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC") -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at http://mozilla.org/MPL/2.0/. -+ -+#include <config.h> -+#include <hooks/hooks.h> -+ -+extern "C" { -+ -+/// @brief returns Kea hooks version. -+int version() { -+ return (KEA_HOOKS_VERSION); -+} -+ -+} --- -2.39.5 (Apple Git-154) - |
