vyos-build/scripts/package-build/kea/patches/isc-kea/0002-Add-ping_check-hook-library.patch

13278 lines
501 KiB
Diff

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)