2
0
mirror of https://gitlab.isc.org/isc-projects/kea synced 2025-08-22 01:49:48 +00:00

[#3333] move hook libraries to open source

This commit is contained in:
Razvan Becheriu 2025-02-12 13:31:51 +02:00
parent 331d0d18c8
commit 785efcc92a
535 changed files with 168637 additions and 205 deletions

42
AUTHORS
View File

@ -7,7 +7,8 @@ Primary developers:
host reservation, MAC extraction in DHCPv6,
statistics manager, kea-shell, netconf, flex/bison
parsers, flex-id, documentation, config backend,
ddns_tuning, CI, documentation)
ddns_tuning, CI, documentation, forensic logging,
host commands)
- Marcin Siodelski (DHCPv4, DHCPv6 components, options handling, perfdhcp,
host reservation, lease file cleanup, lease expiration,
control agent, shared networks, high availability,
@ -47,6 +48,45 @@ Former developers who are no longer active:
Main area of work mentioned in parentheses. The lists are in a roughly
chronological order.
Forensic log (forensic_log)
- Thomas Markwalder, Tomek Mrugalski, Francis Dupont, Marcin Siodelski, Razvan Becheriu
Flex-id (flex_id)
- Tomek Mrugalski, Francis Dupont
Host commands (host_cmds)
- Tomek Mrugalski, Francis Dupont
Subnet commands (subnet_cmds)
- Marcin Siodelski, Thomas Markwalder, Tomek Mrugalski, Francis Dupont
RADIUS (radius)
- Francis Dupont, Andrei Pavel, Razvan Becheriu
Host Caching (host_cache)
- Francis Dupont
Class commands (class_cmds)
- Marcin Siodelski, Tomek Mrugalski, Francis Dupont
Config Backend Commands (cb_cmds)
- Marcin Siodelski, Francis Dupont
Lease Query (lease_query)
- Thomas Markwalder
GSS-TSIG (gss_tsig)
- Francis Dupont, Razvan Becheriu
DDNS-Tuning (ddns_tuning)
- Tomek Mrugalski, Thomas Markwalder
Lease Limiting, Rate Limiting (limits)
- Andrei Pavel
Role Based Access Control (rbac)
- Francis Dupont
Kea uses parts of the code of the now-defunct BIND 10 project. The following
people contributed to BIND 10 code:

View File

@ -1611,30 +1611,72 @@ AC_CONFIG_FILES([src/bin/shell/tests/tls_dhcp4_process_tests.sh],
AC_CONFIG_FILES([src/bin/shell/tests/tls_dhcp6_process_tests.sh],
[chmod +x src/bin/shell/tests/tls_dhcp6_process_tests.sh])
AC_CONFIG_FILES([src/hooks/Makefile])
AC_CONFIG_FILES([src/hooks/d2/Makefile])
AC_CONFIG_FILES([src/hooks/d2/gss_tsig/Makefile])
AC_CONFIG_FILES([src/hooks/d2/gss_tsig/testutils/Makefile])
AC_CONFIG_FILES([src/hooks/d2/gss_tsig/libloadtests/Makefile])
AC_CONFIG_FILES([src/hooks/d2/gss_tsig/tests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/bootp/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/bootp/libloadtests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/bootp/tests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/class_cmds/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/class_cmds/libloadtests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/class_cmds/tests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/ddns_tuning/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/ddns_tuning/libloadtests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/ddns_tuning/tests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/flex_id/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/flex_id/libloadtests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/flex_id/tests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/flex_option/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/flex_option/libloadtests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/flex_option/tests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/forensic_log/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/forensic_log/libloadtests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/forensic_log/tests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/forensic_log/tests/forensic_prerotate_test.sh],
[chmod +x src/hooks/dhcp/forensic_log/tests/forensic_prerotate_test.sh])
AC_CONFIG_FILES([src/hooks/dhcp/forensic_log/tests/forensic_postrotate_test.sh],
[chmod +x src/hooks/dhcp/forensic_log/tests/forensic_postrotate_test.sh])
AC_CONFIG_FILES([src/hooks/dhcp/high_availability/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/high_availability/libloadtests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/high_availability/tests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/host_cache/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/host_cache/libloadtests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/host_cache/tests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/host_cmds/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/host_cmds/libloadtests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/host_cmds/tests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/lease_cmds/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/lease_cmds/libloadtests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/lease_cmds/tests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/lease_query/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/lease_query/libloadtests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/lease_query/tests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/limits/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/limits/libloadtests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/limits/tests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/mysql/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/mysql/libloadtests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/mysql/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/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/pgsql/libloadtests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/pgsql/tests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/radius/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/radius/libloadtests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/radius/tests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/run_script/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/run_script/libloadtests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/run_script/tests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/run_script/tests/run_script_test.sh],
[chmod +x src/hooks/dhcp/run_script/tests/run_script_test.sh])
AC_CONFIG_FILES([src/hooks/dhcp/subnet_cmds/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/subnet_cmds/libloadtests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/subnet_cmds/tests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/stat_cmds/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/stat_cmds/libloadtests/Makefile])
AC_CONFIG_FILES([src/hooks/dhcp/stat_cmds/tests/Makefile])

View File

@ -5,7 +5,7 @@
// parameters available.
//
// To use this configuration file, you need to have both RADIUS and
// Host Cache hooks. These are currently available to support customers only.
// Host Cache hooks.
//
// clients get a wine name (option AOP code 250) divided into red and white.
// Expensive brands have a host entry, i.e. a reserved address.

View File

@ -15,7 +15,7 @@ Windows servers, have chosen to adopt a more complex GSS-TSIG approach that offe
additional capabilities, such as using negotiated dynamic keys.
Kea supports GSS-TSIG to protect DNS updates sent by
the Kea DHCP-DDNS (D2) server in a premium hook, called :ischooklib:`libddns_gss_tsig.so`.
the Kea DHCP-DDNS (D2) server in a hook, called :ischooklib:`libddns_gss_tsig.so`.
GSS-TSIG is defined in `RFC 3645 <https://tools.ietf.org/html/rfc3645>`__.
The GSS-TSIG protocol itself is an implementation of generic GSS-API v2
@ -67,9 +67,7 @@ GSS-TSIG Compilation
The following procedure was tested on Ubuntu 20.10 and 21.04. A similar
approach can be applied to other systems.
1. Obtain the Kea sources and premium packages, extract the Kea sources,
and then extract the premium packages into the ``premium/`` directory within the Kea
source tree.
1. Obtain the Kea sources, extract the Kea sources.
2. Run autoreconf:
@ -117,7 +115,7 @@ detection, similar to this:
available.
7. After compilation, :ischooklib:`libddns_gss_tsig.so` is available in the
``premium/src/hooks/d2/gss_tsig`` directory. It can be loaded by :iscman:`kea-dhcp-ddns`.
``src/hooks/d2/gss_tsig`` directory. It can be loaded by :iscman:`kea-dhcp-ddns`.
:ischooklib:`libddns_gss_tsig.so` was developed using the MIT Kerberos 5 implementation, but
Heimdal is also supported. Note that Heimdal is picky about

View File

@ -12,9 +12,9 @@ list the client classes configured for a given server.
.. note::
:ischooklib:`libdhcp_class_cmds.so` is available only to ISC customers with
a paid support contract. For more information on subscription options,
please complete the form at https://www.isc.org/contact.
:ischooklib:`libdhcp_class_cmds.so` is part of the open source code and is
available to every Kea user.
It was previously available only to ISC customers with a paid support contract.
.. note::

View File

@ -10,10 +10,9 @@ performing DDNS updates for select clients.
.. note::
:ischooklib:`libdhcp_ddns_tuning.so` is available as a premium
hook library from ISC. Please visit https://www.isc.org/shop/ to purchase
the premium hook libraries, or contact us at https://www.isc.org/contact for
more information.
:ischooklib:`libdhcp_ddns_tuning.so` is part of the open source code and is
available to every Kea user.
It was previously available only to ISC customers with a paid support contract.
The library, which was added in Kea 2.1.5, can be loaded by the :iscman:`kea-dhcp4`
and :iscman:`kea-dhcp6` daemons by adding it to the ``hooks-libraries`` element of the

View File

@ -16,10 +16,9 @@ scenarios are addressed by the Flexible Identifiers hook application.
.. note::
:ischooklib:`libdhcp_flex_id.so` is available as a premium
hook library from ISC. Please visit https://www.isc.org/shop/ to purchase
the premium hook libraries, or contact us at https://www.isc.org/contact for
more information.
:ischooklib:`libdhcp_flex_id.so` is part of the open source code and is
available to every Kea user.
It was previously available only to ISC customers with a paid support contract.
.. note::

View File

@ -10,6 +10,6 @@ please see :ref:`gss-tsig`.
.. note::
:ischooklib:`libddns_gss_tsig.so` is available only to ISC customers with
a paid support contract. For more information on subscription options,
please complete the form at https://www.isc.org/contact.
:ischooklib:`libddns_gss_tsig.so` is part of the open source code and is
available to every Kea user.
It was previously available only to ISC customers with a paid support contract.

View File

@ -11,8 +11,8 @@ server.
.. note::
:ischooklib:`libdhcp_ha.so` is part of the open source code and is
available to every Kea user. It was previously available only to ISC
customers with a paid support contract.
available to every Kea user.
It was previously available only to ISC customers with a paid support contract.
.. note::

View File

@ -14,9 +14,9 @@ information in the database.
.. note::
:ischooklib:`libdhcp_host_cache.so` is available only to ISC customers with
a paid support contract. For more information on subscription options,
please complete the form at https://www.isc.org/contact.
:ischooklib:`libdhcp_host_cache.so` is part of the open source code and is
available to every Kea user.
It was previously available only to ISC customers with a paid support contract.
.. note::

View File

@ -16,10 +16,9 @@ interface).
.. note::
:ischooklib:`libdhcp_host_cmds.so` is available as a premium
hook library from ISC. Please visit https://www.isc.org/shop/ to purchase
the premium hook libraries, or contact us at https://www.isc.org/contact for
more information.
:ischooklib:`libdhcp_host_cmds.so` is part of the open source code and is
available to every Kea user.
It was previously available only to ISC customers with a paid support contract.
.. note::

View File

@ -10,9 +10,9 @@ Leasequery as described in (`RFC 5007 <https://tools.ietf.org/html/rfc5007>`__).
.. note::
:ischooklib:`libdhcp_lease_query.so` is available only to ISC customers with
a paid support contract. For more information on subscription options,
please complete the form at https://www.isc.org/contact.
:ischooklib:`libdhcp_lease_query.so` is part of the open source code and is
available to every Kea user.
It was previously available only to ISC customers with a paid support contract.
.. note::

View File

@ -10,10 +10,10 @@ lease events into a set of log files.
.. note::
:ischooklib:`libdhcp_legal_log.so` is available as a premium
hook library from ISC. Please visit https://www.isc.org/shop/ to purchase
the premium hook libraries, or contact us at https://www.isc.org/contact for
more information.
:ischooklib:`libdhcp_legal_log.so` is part of the open source code and is
available to every Kea user.
It was previously available only to ISC customers with a paid support contract.
.. note::

View File

@ -11,9 +11,9 @@ This hook library enables two types of limits:
.. note::
:ischooklib:`libdhcp_limits.so` is available only to ISC customers with
a paid support contract. For more information on subscription options,
please complete the form at https://www.isc.org/contact.
:ischooklib:`libdhcp_limits.so` is part of the open source code and is
available to every Kea user.
It was previously available only to ISC customers with a paid support contract.
.. _hooks-limits-configuration:

View File

@ -11,9 +11,9 @@ to a behavior available in ISC DHCP and one suggested in `RFC
.. note::
:ischooklib:`libdhcp_ping_check.so` is available only to ISC customers
with a paid support contract. For more information on subscription options,
please complete the form at https://www.isc.org/contact.
:ischooklib:`libdhcp_ping_check.so` is part of the open source code and is
available to every Kea user.
It was previously available only to ISC customers with a paid support contract.
Overview
~~~~~~~~

View File

@ -11,6 +11,6 @@ through the accounting service. For details on RADIUS in Kea, please see
.. note::
:ischooklib:`libdhcp_radius.so` is available only to ISC customers with
a paid support contract. For more information on subscription options,
please complete the form at https://www.isc.org/contact.
:ischooklib:`libdhcp_radius.so` is part of the open source code and is
available to every Kea user.
It was previously available only to ISC customers with a paid support contract.

View File

@ -16,9 +16,9 @@ shared networks) is also provided.
.. note::
:ischooklib:`libdhcp_subnet_cmds.so` is available only to ISC customers with
a paid support contract. For more information on subscription options,
please complete the form at https://www.isc.org/contact.
:ischooklib:`libdhcp_subnet_cmds.so` is part of the open source code and is
available to every Kea user.
It was previously available only to ISC customers with a paid support contract.
.. note::

View File

@ -506,8 +506,8 @@ loaded by the correct process per the table below.
| | | they are translated into DHCPREQUEST packets, put into the |
| | | BOOTP client class, and receive infinite lifetime leases. |
+-----------------------------------------------------------+--------------+--------------------------------------------------------------+
| :ref:`Class Commands <hooks-class-cmds>` | ISC support | This hook library allows configured DHCP client classes to |
| | customers | be added, updated, deleted, and fetched without |
| :ref:`Class Commands <hooks-class-cmds>` | Kea open | This hook library allows configured DHCP client classes to |
| | source | be added, updated, deleted, and fetched without |
| | | needing to restart the DHCP server. |
+-----------------------------------------------------------+--------------+--------------------------------------------------------------+
| :ref:`Configuration Backend Commands <hooks-cb-cmds>` | ISC support | This hook |
@ -516,13 +516,13 @@ loaded by the correct process per the table below.
| | | database. This library may only be used in conjunction with |
| | | one of the supported Configuration Backend implementations. |
+-----------------------------------------------------------+--------------+--------------------------------------------------------------+
| :ref:`DDNS Tuning <hooks-ddns-tuning>` | ISC premium | This hook library adds custom behaviors related to Dynamic |
| | library | DNS updates on a per-client basis. Its primary feature is to |
| :ref:`DDNS Tuning <hooks-ddns-tuning>` | Kea open | This hook library adds custom behaviors related to Dynamic |
| | source | DNS updates on a per-client basis. Its primary feature is to |
| | | allow the host name used for DNS to be |
| | | calculated using an expression. |
+-----------------------------------------------------------+--------------+--------------------------------------------------------------+
| :ref:`Flexible Identifier <hooks-flex-id>` | ISC premium | Kea software provides a way to handle host reservations that |
| | library | include addresses, prefixes, options, client classes and |
| :ref:`Flexible Identifier <hooks-flex-id>` | Kea open | Kea software provides a way to handle host reservations that |
| | source | include addresses, prefixes, options, client classes and |
| | | other features. The reservation can be based on hardware |
| | | address, DUID, circuit-id, or client-id in DHCPv4 and on |
| | | hardware address or DUID in DHCPv6. However, there are |
@ -543,8 +543,8 @@ loaded by the correct process per the table below.
| | | remove actions are applied on the response packet before |
| | | it is sent using the evaluation result. |
+-----------------------------------------------------------+--------------+--------------------------------------------------------------+
| :ref:`Forensic Logging <hooks-legal-log>` | ISC premium | This library provides hooks that record a detailed log of |
| | library | lease assignments and renewals in a set of log files. In |
| :ref:`Forensic Logging <hooks-legal-log>` | Kea open | This library provides hooks that record a detailed log of |
| | source | lease assignments and renewals in a set of log files. In |
| | | many legal jurisdictions, companies - especially ISPs - must |
| | | record information about the addresses they have leased to |
| | | DHCP clients. This library is designed to help with that |
@ -556,8 +556,8 @@ loaded by the correct process per the table below.
| | | were added to give users more flexibility regarding |
| | | what information should be logged. |
+-----------------------------------------------------------+--------------+--------------------------------------------------------------+
| :ref:`GSS-TSIG <hooks-gss-tsig>` | ISC support | This hook library adds support to the Kea D2 server |
| | customers | (kea-dhcp-ddns) for using GSS-TSIG to sign DNS updates. |
| :ref:`GSS-TSIG <hooks-gss-tsig>` | Kea open | This hook library adds support to the Kea D2 server |
| | source | (kea-dhcp-ddns) for using GSS-TSIG to sign DNS updates. |
+-----------------------------------------------------------+--------------+--------------------------------------------------------------+
| :ref:`High Availability <hooks-high-availability>` | Kea open | The risk of DHCP service unavailability can be minimized |
| | source | by setting up a pair of DHCP servers in a network. Two |
@ -578,8 +578,8 @@ loaded by the correct process per the table below.
| | | to send lease updates to external backup servers, making it |
| | | much easier to have a replacement that is up to date. |
+-----------------------------------------------------------+--------------+--------------------------------------------------------------+
| :ref:`Host Cache <hooks-host-cache>` | ISC support | Some database backends, such as RADIUS, |
| | customers | may take a long time to respond. Since |
| :ref:`Host Cache <hooks-host-cache>` | Kea open | Some database backends, such as RADIUS, |
| | source | may take a long time to respond. Since |
| | | Kea in general is synchronous, backend performance |
| | | directly affects DHCP performance. To minimize |
| | | performance impact, this library |
@ -587,8 +587,8 @@ loaded by the correct process per the table below.
| | | includes negative caching, i.e. the ability to remember that |
| | | there is no client information in the database. |
+-----------------------------------------------------------+--------------+--------------------------------------------------------------+
| :ref:`Host Commands <hooks-host-cmds>` | ISC premium | Kea provides a way to store host reservations in a |
| | library | database. In many larger deployments it is useful to be able |
| :ref:`Host Commands <hooks-host-cmds>` | Kea open | Kea provides a way to store host reservations in a |
| | source | database. In many larger deployments it is useful to be able |
| | | to manage that information while the server is running. This |
| | | library provides management commands for adding, querying, |
| | | and deleting host reservations in a safe way without |
@ -613,12 +613,12 @@ loaded by the correct process per the table below.
| | | belong. This library allows easy management of user contexts |
| | | associated with leases. |
+-----------------------------------------------------------+--------------+--------------------------------------------------------------+
| :ref:`Leasequery <hooks-lease-query>` | ISC support | This library adds support for DHCPv4 Leasequery (RFC 4388), |
| | customers | DHCPv4 Bulk Leasequery (RFC6926); DHCPv6 Leasequery |
| :ref:`Leasequery <hooks-lease-query>` | Kea open | This library adds support for DHCPv4 Leasequery (RFC 4388), |
| | source | DHCPv4 Bulk Leasequery (RFC6926); DHCPv6 Leasequery |
| | | (RFC 5007), and DHCPv6 Bulk Leasequery (RFC5460). |
+-----------------------------------------------------------+--------------+--------------------------------------------------------------+
| :ref:`Limits <hooks-limits>` | ISC support | With this hook library, :iscman:`kea-dhcp4` and |
| | customers | :iscman:`kea-dhcp6` servers can apply a limit to the rate at |
| :ref:`Limits <hooks-limits>` | Kea open | With this hook library, :iscman:`kea-dhcp4` and |
| | source | :iscman:`kea-dhcp6` servers can apply a limit to the rate at |
| | | which packets receive a response. The limit can be applied |
| | | per-client class or per-subnet. |
+-----------------------------------------------------------+--------------+--------------------------------------------------------------+
@ -633,9 +633,9 @@ loaded by the correct process per the table below.
| CURRENTLY EXPERIMENTAL | source | :iscman:`kea-dhcp6` servers can track and report performance |
| | | data. |
+-----------------------------------------------------------+--------------+--------------------------------------------------------------+
| :ref:`Ping Check <hooks-ping-check>` | ISC support | With this hook library, the :iscman:`kea-dhcp4` server can |
| :ref:`Ping Check <hooks-ping-check>` | Kea open | With this hook library, the :iscman:`kea-dhcp4` server can |
| | source | perform ping checks of candidate lease addresses before |
| | customers | offering them to clients. |
| | | offering them to clients. |
+-----------------------------------------------------------+--------------+--------------------------------------------------------------+
| :ref:`PostgreSQL Database Backend <hooks-pgsql>` | Kea open | This hook library is an implementation of the Kea Lease, |
| | source | Host and Configuration Backend for PostgreSQL. It uses a |
@ -644,8 +644,8 @@ loaded by the correct process per the table below.
| | | this library to fetch their configurations if Configuration |
| | | Backend is used. |
+-----------------------------------------------------------+--------------+--------------------------------------------------------------+
| :ref:`RADIUS <hooks-radius>` | ISC support | The RADIUS hook library allows Kea to interact with |
| | customers | RADIUS servers using access and accounting mechanisms. The |
| :ref:`RADIUS <hooks-radius>` | Kea open | The RADIUS hook library allows Kea to interact with |
| | source | RADIUS servers using access and accounting mechanisms. The |
| | | access mechanism may be used for access control, assigning |
| | | specific IPv4 or IPv6 addresses reserved by RADIUS, |
| | | dynamically assigning addresses from designated pools chosen |
@ -654,8 +654,9 @@ loaded by the correct process per the table below.
| | | track of device activity over time. |
+-----------------------------------------------------------+--------------+--------------------------------------------------------------+
| :ref:`RBAC <hooks-rbac>` | ISC support | This hook library adds support to the Kea Control Agent |
| | customers | (kea-ctrl-agent) for Role-Based Access Control filtering |
| | | of commands. |
| | customers | (kea-ctrl-agent) and Kea servers (kea-dhcp4, kea-dhcp6 and |
| | | kea-dhcp-ddns) for Role-Based Access Control filtering of |
| | | commands. |
+-----------------------------------------------------------+--------------+--------------------------------------------------------------+
| :ref:`Run Script <hooks-run-script>` | Kea open | This hook library adds support to run external |
| | source | scripts for specific packet-processing hook points. There |
@ -671,8 +672,8 @@ loaded by the correct process per the table below.
| | | This hook library returns lease statistics |
| | | for each subnet. |
+-----------------------------------------------------------+--------------+--------------------------------------------------------------+
| :ref:`Subnet Commands <hooks-subnet-cmds>` | ISC support | In deployments in which subnet configuration needs to be |
| | customers | frequently updated, it is a hard requirement that such |
| :ref:`Subnet Commands <hooks-subnet-cmds>` | Kea open | In deployments in which subnet configuration needs to be |
| | source | frequently updated, it is a hard requirement that such |
| | | updates be performed without the need for a full DHCP server |
| | | reconfiguration or restart. This hook library allows for |
| | | incremental changes to the subnet configuration such as |

View File

@ -162,7 +162,7 @@ libraries), or hook libraries (open source or premium).
| | | messages. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-ctrl-agent.rbac-hooks`` | :ischooklib:`libdhcp_rbac.so` | Used to log messages related to|
| | enterprise hook library | the operation of the RBAC hook |
| | premium hook library | the operation of the RBAC hook |
| | | library. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp4`` | core | The root logger for the DHCPv4 |
@ -209,11 +209,11 @@ libraries), or hook libraries (open source or premium).
| ``kea-dhcp-ddns.callouts`` | | hook point. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp4.cb-cmds-hooks``, | :ischooklib:`libdhcp_cb_cmds.so` | Used to log messages related to|
| ``kea-dhcp6.cb-cmds-hooks`` | subscription hook library | the operation of the Config |
| ``kea-dhcp6.cb-cmds-hooks`` | premium hook library | the operation of the Config |
| | | Backend Commands hook library. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp4.class-cmds-hooks``, | :ischooklib:`libdhcp_class_cmds.so` | Used to log messages related to|
| ``kea-dhcp6.class-cmds-hooks`` | subscription hook library | the operation of the Class |
| ``kea-dhcp6.class-cmds-hooks`` | open-source hook library | the operation of the Class |
| | | Commands hook library. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp4.commands``, | core | Used to log messages related to|
@ -226,7 +226,7 @@ libraries), or hook libraries (open source or premium).
| | | relational databases. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp4.ddns-tuning-hooks``, | :ischooklib:`libdhcp_ddns_tuning.so` | Used to log messages related to|
| ``kea-dhcp6.ddns-tuning-hooks`` | premium hook library | the operation of the DDNS |
| ``kea-dhcp6.ddns-tuning-hooks`` | open-source hook library | the operation of the DDNS |
| | | Tuning hook library. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp4.ddns``, | core | Used by the DHCP server to log|
@ -247,7 +247,7 @@ libraries), or hook libraries (open source or premium).
| | | expression evaluation code. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp4.flex-id-hooks``, | :ischooklib:`libdhcp_flex_id.so` | Used to log messages related to|
| ``kea-dhcp6.flex-id-hooks`` | premium hook library | the operation of the Flexible |
| ``kea-dhcp6.flex-id-hooks`` | open-source hook library | the operation of the Flexible |
| | | Identifier hook library. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp4.flex-option-hooks``, | :ischooklib:`libdhcp_flex_option.so` | Used to log messages related to|
@ -268,11 +268,11 @@ libraries), or hook libraries (open source or premium).
| | | points within the DHCP server. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp4.host-cache-hooks``, | :ischooklib:`libdhcp_host_cache.so` | Used to log messages related to|
| ``kea-dhcp6.host-cache-hooks`` | subscription hook library | the operation of the Host Cache|
| ``kea-dhcp6.host-cache-hooks`` | open-source hook library | the operation of the Host Cache|
| | | hook library. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp4.host-cmds-hooks``, | :ischooklib:`libdhcp_host_cmds.so` | Used to log messages related to|
| ``kea-dhcp6.host-cmds-hooks`` | premium hook library | the operation of the Host |
| ``kea-dhcp6.host-cmds-hooks`` | open-source hook library | the operation of the Host |
| | | Commands hook library. In |
| | | general, these pertain to the |
| | | loading and unloading of the |
@ -296,11 +296,11 @@ libraries), or hook libraries (open source or premium).
| | | commands by the library. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp4.limits-hooks``, | :ischooklib:`libdhcp_limits.so` | Used to log messages related to|
| ``kea-dhcp6.limits-hooks`` | subscription hook library | the operation of the Limits |
| ``kea-dhcp6.limits-hooks`` | open-source hook library | the operation of the Limits |
| | | hook library. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp4.lease-query-hooks``, | :ischooklib:`libdhcp_lease_query.so` | Used to log messages related to|
| ``kea-dhcp6.lease-query-hooks`` | premium hook library | the operation of the Leasequery|
| ``kea-dhcp6.lease-query-hooks`` | open-source hook library | the operation of the Leasequery|
| | | hook library. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp4.leases``, | core | Used by the DHCP server to log |
@ -312,7 +312,7 @@ libraries), or hook libraries (open source or premium).
| | | allocation, etc. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp4.legal-log-hooks``, | :ischooklib:`libdhcp_legal_log.so` | Used to log messages related to|
| ``kea-dhcp6.legal-log-hooks`` | premium hook library | the operation of the Forensic |
| ``kea-dhcp6.legal-log-hooks`` | open-source hook library | the operation of the Forensic |
| | | Logging hook library. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp4.mysql-cb-hooks``, | :ischooklib:`libdhcp_mysql.so` | Used to log messages related to|
@ -363,7 +363,7 @@ libraries), or hook libraries (open source or premium).
| | | library. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp4.ping-check-hooks`` | :ischooklib:`libdhcp_ping_check.so` | Used to log messages related to|
| | subscription hook library | the operation of the Ping Check|
| | open-source hook library | the operation of the Ping Check|
| | | hook library. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp4.pgsql-cb-hooks``, | :ischooklib:`libdhcp_pgsql.so` | Used to log messages related to|
@ -384,7 +384,7 @@ libraries), or hook libraries (open source or premium).
| | | hook library. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp4.radius-hooks``, | :ischooklib:`libdhcp_radius.so` | Used to log messages related to|
| ``kea-dhcp6.radius-hooks`` | premium hook library | the operation of the RADIUS |
| ``kea-dhcp6.radius-hooks`` | open-source hook library | the operation of the RADIUS |
| | | hook library. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp4.stat-cmds-hooks``, | :ischooklib:`libdhcp_stat_cmds.so` | Used to log messages related to|
@ -396,7 +396,7 @@ libraries), or hook libraries (open source or premium).
| | | commands by the library. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp4.subnet-cmds-hooks``, | :ischooklib:`libdhcp_subnet_cmds.so` | Used to log messages related to|
| ``kea-dhcp6.subnet-cmds-hooks`` | premium hook library | the operation of the Subnet |
| ``kea-dhcp6.subnet-cmds-hooks`` | open-source hook library | the operation of the Subnet |
| | | Commands hook library. In |
| | | general, these pertain to |
| | | loading and unloading the |
@ -407,7 +407,7 @@ libraries), or hook libraries (open source or premium).
| ``kea-dhcp6.tcp`` | | TCP traffic. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp4.user_chk``, | :ischooklib:`libdhcp_user_chk.so` | Used to log messages related to|
| ``kea-dhcp6.user_chk`` | hook library | the operation of the User Check|
| ``kea-dhcp6.user_chk`` | open-source hook library | the operation of the User Check|
| | | hook library. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp-ddns`` | core | The root logger for the |
@ -448,7 +448,7 @@ libraries), or hook libraries (open source or premium).
| | | the DNS servers. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp-ddns.gss-tsig-hooks`` | :ischooklib:`libddns_gss_tsig.so` | Used to log messages related to|
| | subscription hook library | the operation of the GSS-TSIG |
| | open-source hook library | the operation of the GSS-TSIG |
| | | hook library. |
+----------------------------------+---------------------------------------+--------------------------------+
| ``kea-dhcp-ddns.libdhcp-ddns`` | core | Used to log events related to |

View File

@ -2603,7 +2603,7 @@ COMMAND_HTTP_LISTENER_STARTED
.. code-block:: text
Command HTTP listener started with %1 threads, listening on %2:%3, use TLS: %4
Command HTTP listener started with %1 threads, listening on address: %2 port: %3, use TLS: %4
Logged at debug log level 10.
This debug messages is issued when an HTTP listener has been started to
@ -2617,7 +2617,7 @@ COMMAND_HTTP_LISTENER_STOPPED
.. code-block:: text
Command HTTP listener for %1:%2 stopped.
Command HTTP listener for address: %1 port: %2 stopped.
Logged at debug log level 10.
This debug messages is issued when the Command HTTP listener, listening
@ -2628,7 +2628,7 @@ COMMAND_HTTP_LISTENER_STOPPING
.. code-block:: text
Stopping Command HTTP listener for %1:%2
Stopping Command HTTP listener for address: %1 port: %2
Logged at debug log level 10.
This debug messages is issued when the Command HTTP listener, listening
@ -2948,49 +2948,71 @@ CTRL_AGENT_FAILED
This is a fatal error message issued when the Control Agent application
encounters an unrecoverable error from within the event loop.
CTRL_AGENT_HTTPS_SERVICE_REUSED
===============================
CTRL_AGENT_HTTPS_SERVICE_REUSE_FAILED
=====================================
.. code-block:: text
reused HTTPS service bound to address %1:%2
failed to reuse HTTPS service bound to address: %1 port: %2
This informational message indicates that the server has reused existing
HTTPS service on the specified address and port. Note that any change in
the TLS setup was ignored.
This error message indicates that the server has failed reusing existing
HTTPS service on the specified address and port. The server can not swith from
HTTPS to HTTP sockets using the same address and port.
CTRL_AGENT_HTTPS_SERVICE_STARTED
================================
.. code-block:: text
HTTPS service bound to address %1:%2
HTTPS service bound to address: %1 port: %2
This informational message indicates that the server has started HTTPS service
on the specified address and port. All control commands should be sent to this
address and port over a TLS channel.
CTRL_AGENT_HTTP_SERVICE_REUSED
==============================
CTRL_AGENT_HTTPS_SERVICE_UPDATED
================================
.. code-block:: text
reused HTTP service bound to address %1:%2
reused HTTPS service bound to address: %1 port: %2 and updated TLS settings
This informational message indicates that the server has reused existing
HTTPS service on the specified address and port.
HTTPS service on the specified address and port. Note that any change in
the TLS setup has been applied.
CTRL_AGENT_HTTP_SERVICE_REUSE_FAILED
====================================
.. code-block:: text
failed to reused HTTP service bound to address: %1 port: %2
This error message indicates that the server has failed reusing existing
HTTP service on the specified address and port. The server can not swith from
HTTP to HTTPS sockets using the same address and port.
CTRL_AGENT_HTTP_SERVICE_STARTED
===============================
.. code-block:: text
HTTP service bound to address %1:%2
HTTP service bound to address: %1 port: %2
This informational message indicates that the server has started HTTP service
on the specified address and port. All control commands should be sent to this
address and port.
CTRL_AGENT_HTTP_SERVICE_UPDATED
===============================
.. code-block:: text
reused HTTP service bound to address: %1 port: %2
This informational message indicates that the server has reused existing
HTTP service on the specified address and port.
CTRL_AGENT_RUN_EXIT
===================
@ -15936,49 +15958,60 @@ This debug message is issued when the HTTP request timeout has occurred and
the server is going to send a response with Http Request timeout status
code.
HTTP_COMMAND_MGR_IGNORED_TLS_SETUP_CHANGES
HTTP_COMMAND_MGR_HTTPS_SERVICE_REUSE_FAILED
===========================================
.. code-block:: text
failed to reused HTTPS service bound to address: %1 port: %2
This error message indicates that the server has failed reusing existing
HTTPS service on the specified address and port. The server can not swith from
HTTPS to HTTP sockets using the same address and port.
HTTP_COMMAND_MGR_HTTPS_SERVICE_UPDATED
======================================
.. code-block:: text
reused HTTPS service bound to address: %1 port: %2 and updated TLS settings
This informational message indicates that the server has reused existing
HTTPS service on the specified address and port. Note that any change in
the TLS setup has been applied.
HTTP_COMMAND_MGR_HTTP_SERVICE_REUSE_FAILED
==========================================
.. code-block:: text
ignore a change in TLS setup of the http control socket
failed to reused HTTP service bound to address: %1 port: %2
The warning message is issued when the HTTP/HTTPS control socket was
reconfigured with a different TLS setup but keeping the address and port.
These changes are ignored because they can't be applied without opening a new
socket which will conflict with the existing one.
This error message indicates that the server has failed reusing existing
HTTP service on the specified address and port. The server can not swith from
HTTP to HTTPS sockets using the same address and port.
HTTP_COMMAND_MGR_HTTP_SERVICE_UPDATED
=====================================
.. code-block:: text
reused HTTP service bound to address: %1 port: %2
This informational message indicates that the server has reused existing
HTTP service on the specified address and port.
HTTP_COMMAND_MGR_SERVICE_STARTED
================================
.. code-block:: text
started %1 service bound to address %2 port %3
started %1 service bound to address: %2 port: %3
This informational message indicates that the server has started
HTTP/HTTPS service on the specified address and port for receiving
control commands.
HTTP_COMMAND_MGR_SERVICE_STOPPING
=================================
.. code-block:: text
Server is stopping %1 service %2
This informational message indicates that the server has stopped
HTTP/HTTPS service. When known the address and port are displayed.
HTTP_COMMAND_MGR_SERVICE_STOPPING_ALL
=====================================
.. code-block:: text
stopping %1 service %2
This informational message indicates that the server has stopped
HTTP/HTTPS service. When known the address and port are displayed.
HTTP_CONNECTION_CLOSE_CALLBACK_FAILED
=====================================
@ -16408,6 +16441,61 @@ LEASE_CMDS_INIT_OK
This info message indicates that the Lease Commands hooks library has been
loaded successfully. Enjoy!
LEASE_CMDS_LEASE4_OFFER_FAILED
==============================
.. code-block:: text
processing error occurred evaluating binding variables: %1
This error log is emitted when an error occurs in the lease4_offer
handler is invoked. The argument provides an explanation.
LEASE_CMDS_LEASES4_COMMITTED_FAILED
===================================
.. code-block:: text
processing error occurred evaluating binding variables: %1
This error log is emitted when an error occurs in the leases4_committed
handler is invoked. The argument provides an explanation.
LEASE_CMDS_LEASES6_COMMITTED_CONFLICT
=====================================
.. code-block:: text
could not updating lease: %1 for: %2
This error log is emitted by the leases6_committed callback when attempting
to update a lease with new binding-variable values but a conflicting change
has occurred rendering the update invalid. The arguments provide the lease
address and the query details.
LEASE_CMDS_LEASES6_COMMITTED_FAILED
===================================
.. code-block:: text
reason: %1
This error log is emitted when one or more leases associated with a client
query failed to be updated with binding-variable values. The argument
provides details. Individual errors for each lease should precede this log.
LEASE_CMDS_LEASES6_COMMITTED_LEASE_ERROR
========================================
.. code-block:: text
evaluating binding-variables for lease: %1 for: %2, reason: %3
This error log is emitted by the leases6_committed callback when an
unexpected error occurs evaluating the binding-variables for a given
lease. The arguments provide the lease address, the query details, and
an error explanation.
LEASE_CMDS_RESEND_DDNS4
=======================
@ -16562,6 +16650,16 @@ LEASE_CMDS_WIPE6_DEPRECATED
The lease6-wipe command is deprecated and it will be removed in the future.
LEASE_CMDS_WIPE6_FAILED
=======================
.. code-block:: text
lease6-wipe command failed (parameters: %1, reason: %2)
The lease6-wipe command has failed. Both the reason as well as the
parameters passed are logged.
LEASE_QUERY_LOAD_FAILED
=======================

View File

@ -1,26 +1,23 @@
premium/src/hooks/d2/gss_tsig/gss_tsig_messages.mes
premium/src/hooks/dhcp/cb_cmds/cb_cmds_messages.mes
premium/src/hooks/dhcp/class_cmds/class_cmds_messages.mes
premium/src/hooks/dhcp/ddns_tuning/ddns_tuning_messages.mes
premium/src/hooks/dhcp/flex_id/flex_id_messages.mes
premium/src/hooks/dhcp/forensic_log/legal_log_messages.mes
premium/src/hooks/dhcp/host_cache/host_cache_messages.mes
premium/src/hooks/dhcp/host_cmds/host_cmds_messages.mes
premium/src/hooks/dhcp/lease_query/lease_query_messages.mes
premium/src/hooks/dhcp/limits/limits_messages.mes
premium/src/hooks/dhcp/ping_check/ping_check_messages.mes
premium/src/hooks/dhcp/radius/radius_messages.mes
premium/src/hooks/dhcp/rbac/rbac_messages.mes
premium/src/hooks/dhcp/subnet_cmds/subnet_cmds_messages.mes
src/bin/agent/ca_messages.mes
src/bin/dhcp4/dhcp4_messages.mes
src/bin/dhcp6/dhcp6_messages.mes
src/bin/lfc/lfc_messages.mes
src/bin/netconf/netconf_messages.mes
src/hooks/d2/gss_tsig/gss_tsig_messages.mes
src/hooks/dhcp/bootp/bootp_messages.mes
src/hooks/dhcp/class_cmds/class_cmds_messages.mes
src/hooks/dhcp/ddns_tuning/ddns_tuning_messages.mes
src/hooks/dhcp/flex_id/flex_id_messages.mes
src/hooks/dhcp/flex_option/flex_option_messages.mes
src/hooks/dhcp/forensic_log/legal_log_messages.mes
src/hooks/dhcp/high_availability/ha_messages.mes
src/hooks/dhcp/host_cache/host_cache_messages.mes
src/hooks/dhcp/host_cmds/host_cmds_messages.mes
src/hooks/dhcp/lease_cmds/lease_cmds_messages.mes
src/hooks/dhcp/lease_query/lease_query_messages.mes
src/hooks/dhcp/limits/limits_messages.mes
src/hooks/dhcp/mysql/mysql_cb_messages.mes
src/hooks/dhcp/mysql/mysql_hb_messages.mes
src/hooks/dhcp/mysql/mysql_lb_messages.mes
@ -28,8 +25,11 @@ src/hooks/dhcp/perfmon/perfmon_messages.mes
src/hooks/dhcp/pgsql/pgsql_cb_messages.mes
src/hooks/dhcp/pgsql/pgsql_hb_messages.mes
src/hooks/dhcp/pgsql/pgsql_lb_messages.mes
src/hooks/dhcp/ping_check/ping_check_messages.mes
src/hooks/dhcp/radius/radius_messages.mes
src/hooks/dhcp/run_script/run_script_messages.mes
src/hooks/dhcp/stat_cmds/stat_cmds_messages.mes
src/hooks/dhcp/subnet_cmds/subnet_cmds_messages.mes
src/hooks/dhcp/user_chk/user_chk_messages.mes
src/lib/asiodns/asiodns_messages.mes
src/lib/config/config_messages.mes

View File

@ -1,26 +1,23 @@
mes_files += $(top_srcdir)/premium/src/hooks/d2/gss_tsig/gss_tsig_messages.mes
mes_files += $(top_srcdir)/premium/src/hooks/dhcp/cb_cmds/cb_cmds_messages.mes
mes_files += $(top_srcdir)/premium/src/hooks/dhcp/class_cmds/class_cmds_messages.mes
mes_files += $(top_srcdir)/premium/src/hooks/dhcp/ddns_tuning/ddns_tuning_messages.mes
mes_files += $(top_srcdir)/premium/src/hooks/dhcp/flex_id/flex_id_messages.mes
mes_files += $(top_srcdir)/premium/src/hooks/dhcp/forensic_log/legal_log_messages.mes
mes_files += $(top_srcdir)/premium/src/hooks/dhcp/host_cache/host_cache_messages.mes
mes_files += $(top_srcdir)/premium/src/hooks/dhcp/host_cmds/host_cmds_messages.mes
mes_files += $(top_srcdir)/premium/src/hooks/dhcp/lease_query/lease_query_messages.mes
mes_files += $(top_srcdir)/premium/src/hooks/dhcp/limits/limits_messages.mes
mes_files += $(top_srcdir)/premium/src/hooks/dhcp/ping_check/ping_check_messages.mes
mes_files += $(top_srcdir)/premium/src/hooks/dhcp/radius/radius_messages.mes
mes_files += $(top_srcdir)/premium/src/hooks/dhcp/rbac/rbac_messages.mes
mes_files += $(top_srcdir)/premium/src/hooks/dhcp/subnet_cmds/subnet_cmds_messages.mes
mes_files += $(top_srcdir)/src/bin/agent/ca_messages.mes
mes_files += $(top_srcdir)/src/bin/dhcp4/dhcp4_messages.mes
mes_files += $(top_srcdir)/src/bin/dhcp6/dhcp6_messages.mes
mes_files += $(top_srcdir)/src/bin/lfc/lfc_messages.mes
mes_files += $(top_srcdir)/src/bin/netconf/netconf_messages.mes
mes_files += $(top_srcdir)/src/hooks/d2/gss_tsig/gss_tsig_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/bootp/bootp_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/class_cmds/class_cmds_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/ddns_tuning/ddns_tuning_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/flex_id/flex_id_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/flex_option/flex_option_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/forensic_log/legal_log_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/high_availability/ha_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/host_cache/host_cache_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/host_cmds/host_cmds_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/lease_cmds/lease_cmds_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/lease_query/lease_query_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/limits/limits_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/mysql/mysql_cb_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/mysql/mysql_hb_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/mysql/mysql_lb_messages.mes
@ -28,8 +25,11 @@ mes_files += $(top_srcdir)/src/hooks/dhcp/perfmon/perfmon_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/pgsql/pgsql_cb_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/pgsql/pgsql_hb_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/pgsql/pgsql_lb_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/ping_check/ping_check_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/radius/radius_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/run_script/run_script_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/stat_cmds/stat_cmds_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/subnet_cmds/subnet_cmds_messages.mes
mes_files += $(top_srcdir)/src/hooks/dhcp/user_chk/user_chk_messages.mes
mes_files += $(top_srcdir)/src/lib/asiodns/asiodns_messages.mes
mes_files += $(top_srcdir)/src/lib/config/config_messages.mes

View File

@ -1,6 +1,6 @@
#line 1 "dhcp4_lexer.cc"
#line 2 "dhcp4_lexer.cc"
#line 3 "dhcp4_lexer.cc"
#line 4 "dhcp4_lexer.cc"
#define YY_INT_ALIGNED short int
@ -2278,7 +2278,7 @@ using namespace isc::dhcp;
/* To avoid the call to exit... oops! */
#define YY_FATAL_ERROR(msg) isc::dhcp::Parser4Context::fatal(msg)
#line 2281 "dhcp4_lexer.cc"
#line 2282 "dhcp4_lexer.cc"
/* noyywrap disables automatic rewinding for the next file to parse. Since we
always parse only a single string, there's no need to do any wraps. And
using yywrap requires linking with -lfl, which provides the default yywrap
@ -2304,8 +2304,8 @@ using namespace isc::dhcp;
by moving it ahead by yyleng bytes. yyleng specifies the length of the
currently matched token. */
#define YY_USER_ACTION driver.loc_.columns(yyleng);
#line 2307 "dhcp4_lexer.cc"
#line 2308 "dhcp4_lexer.cc"
#line 2309 "dhcp4_lexer.cc"
#define INITIAL 0
#define COMMENT 1
@ -2633,7 +2633,7 @@ YY_DECL
}
#line 2636 "dhcp4_lexer.cc"
#line 2637 "dhcp4_lexer.cc"
while ( /*CONSTCOND*/1 ) /* loops until end-of-file is reached */
{
@ -5887,7 +5887,7 @@ YY_RULE_SETUP
#line 2575 "dhcp4_lexer.ll"
ECHO;
YY_BREAK
#line 5890 "dhcp4_lexer.cc"
#line 5891 "dhcp4_lexer.cc"
case YY_END_OF_BUFFER:
{

View File

@ -740,6 +740,11 @@ public:
/// test to make sure that contents of the database do not affect the
/// results of subsequent tests.
void resetConfiguration() {
// The default setting is to listen on all interfaces. In order to
// properly test interface configuration we disable listening on
// all interfaces before each test and later check that this setting
// has been overridden by the configuration used in the test.
CfgMgr::instance().clear();
string config = "{ \"interfaces-config\": {"
" \"interfaces\": [ ]"
"},"
@ -753,11 +758,7 @@ public:
"\"option-data\": [ ] }";
static_cast<void>(executeConfiguration(config,
"reset configuration database"));
// The default setting is to listen on all interfaces. In order to
// properly test interface configuration we disable listening on
// all interfaces before each test and later check that this setting
// has been overridden by the configuration used in the test.
CfgMgr::instance().clear();
CfgMgr::instance().clearStagingConfiguration();
}
/// @brief Retrieve an option associated with a host.

View File

@ -1,6 +1,6 @@
#line 1 "dhcp6_lexer.cc"
#line 2 "dhcp6_lexer.cc"
#line 3 "dhcp6_lexer.cc"
#line 4 "dhcp6_lexer.cc"
#define YY_INT_ALIGNED short int
@ -2251,7 +2251,7 @@ using namespace isc::dhcp;
/* To avoid the call to exit... oops! */
#define YY_FATAL_ERROR(msg) isc::dhcp::Parser6Context::fatal(msg)
#line 2254 "dhcp6_lexer.cc"
#line 2255 "dhcp6_lexer.cc"
/* noyywrap disables automatic rewinding for the next file to parse. Since we
always parse only a single string, there's no need to do any wraps. And
using yywrap requires linking with -lfl, which provides the default yywrap
@ -2277,8 +2277,8 @@ using namespace isc::dhcp;
by moving it ahead by yyleng bytes. yyleng specifies the length of the
currently matched token. */
#define YY_USER_ACTION driver.loc_.columns(yyleng);
#line 2280 "dhcp6_lexer.cc"
#line 2281 "dhcp6_lexer.cc"
#line 2282 "dhcp6_lexer.cc"
#define INITIAL 0
#define COMMENT 1
@ -2608,7 +2608,7 @@ YY_DECL
}
#line 2611 "dhcp6_lexer.cc"
#line 2612 "dhcp6_lexer.cc"
while ( /*CONSTCOND*/1 ) /* loops until end-of-file is reached */
{
@ -5902,7 +5902,7 @@ YY_RULE_SETUP
#line 2608 "dhcp6_lexer.ll"
ECHO;
YY_BREAK
#line 5905 "dhcp6_lexer.cc"
#line 5906 "dhcp6_lexer.cc"
case YY_END_OF_BUFFER:
{

View File

@ -869,6 +869,11 @@ public:
/// test to make sure that contents of the database do not affect the
/// results of subsequent tests.
void resetConfiguration() {
// The default setting is to listen on all interfaces. In order to
// properly test interface configuration we disable listening on
// all interfaces before each test and later check that this setting
// has been overridden by the configuration used in the test.
CfgMgr::instance().clear();
string config = "{ \"interfaces-config\": {"
" \"interfaces\": [ ]"
"},"
@ -883,11 +888,7 @@ public:
"\"option-data\": [ ] }";
static_cast<void>(executeConfiguration(config,
"reset configuration database"));
// The default setting is to listen on all interfaces. In order to
// properly test interface configuration we disable listening on
// all interfaces before each test and later check that this setting
// has been overridden by the configuration used in the test.
CfgMgr::instance().clear();
CfgMgr::instance().clearStagingConfiguration();
}
/// @brief Retrieve an option associated with a host.

View File

@ -1 +1 @@
SUBDIRS = dhcp
SUBDIRS = dhcp d2

1
src/hooks/d2/Makefile.am Normal file
View File

@ -0,0 +1 @@
SUBDIRS = gss_tsig

2
src/hooks/d2/gss_tsig/.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
/gss_tsig_messages.cc -diff merge=ours
/gss_tsig_messages.h -diff merge=ours

1
src/hooks/d2/gss_tsig/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/html

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
# This is a doxygen configuration for generating XML output as well as HTML.
#
# Inherit everything from our default Doxyfile except GENERATE_XML, which
# will be reset to YES
@INCLUDE = Doxyfile
GENERATE_XML = YES

View File

@ -0,0 +1,99 @@
if HAVE_GSSAPI
SUBDIRS = . testutils libloadtests tests
AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
AM_CPPFLAGS += $(GSSAPI_CFLAGS) $(BOOST_INCLUDES)
AM_CPPFLAGS += $(CRYPTO_CFLAGS) $(CRYPTO_INCLUDES)
AM_CXXFLAGS = $(KEA_CXXFLAGS)
# Ensure that the message file is included in the distribution
EXTRA_DIST = gss_tsig_messages.mes
EXTRA_DIST += gss_tsig.dox Doxyfile Doxyfile-xml
CLEANFILES = *.gcno *.gcda
# convenience archive
noinst_LTLIBRARIES = libgss_tsig.la
libgss_tsig_la_SOURCES = gss_tsig_callouts.cc
libgss_tsig_la_SOURCES += gss_tsig_cfg.cc gss_tsig_cfg.h
libgss_tsig_la_SOURCES += gss_tsig_context.cc gss_tsig_context.h
libgss_tsig_la_SOURCES += gss_tsig_impl.cc gss_tsig_impl.h
libgss_tsig_la_SOURCES += gss_tsig_key.cc gss_tsig_key.h
libgss_tsig_la_SOURCES += gss_tsig_log.cc gss_tsig_log.h
libgss_tsig_la_SOURCES += gss_tsig_messages.cc gss_tsig_messages.h
libgss_tsig_la_SOURCES += gss_tsig_api.cc gss_tsig_api.h
libgss_tsig_la_SOURCES += managed_key.cc managed_key.h
libgss_tsig_la_SOURCES += tkey_exchange.cc tkey_exchange.h
libgss_tsig_la_SOURCES += version.cc
libgss_tsig_la_CXXFLAGS = $(AM_CXXFLAGS)
libgss_tsig_la_CPPFLAGS = $(AM_CPPFLAGS)
# install the shared object into $(libdir)/kea/hooks
lib_hooksdir = $(libdir)/kea/hooks
lib_hooks_LTLIBRARIES = libddns_gss_tsig.la
libddns_gss_tsig_la_SOURCES =
libddns_gss_tsig_la_LDFLAGS = $(AM_LDFLAGS)
libddns_gss_tsig_la_LDFLAGS += -avoid-version -export-dynamic -module
libddns_gss_tsig_la_LIBADD = libgss_tsig.la
libddns_gss_tsig_la_LIBADD += $(top_builddir)/src/lib/d2srv/libkea-d2srv.la
libddns_gss_tsig_la_LIBADD += $(top_builddir)/src/lib/asiodns/libkea-asiodns.la
libddns_gss_tsig_la_LIBADD += $(top_builddir)/src/lib/stats/libkea-stats.la
libddns_gss_tsig_la_LIBADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la
libddns_gss_tsig_la_LIBADD += $(top_builddir)/src/lib/cc/libkea-cc.la
libddns_gss_tsig_la_LIBADD += $(top_builddir)/src/lib/dns/libkea-dns++.la
libddns_gss_tsig_la_LIBADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la
libddns_gss_tsig_la_LIBADD += $(top_builddir)/src/lib/log/libkea-log.la
libddns_gss_tsig_la_LIBADD += $(top_builddir)/src/lib/util/libkea-util.la
libddns_gss_tsig_la_LIBADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
libddns_gss_tsig_la_LIBADD += $(GSSAPI_LIBS)
libddns_gss_tsig_la_LIBADD += $(LOG4CPLUS_LIBS)
libddns_gss_tsig_la_LIBADD += $(CRYPTO_LIBS)
libddns_gss_tsig_la_LIBADD += $(BOOST_LIBS)
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 gss_tsig_messages.h gss_tsig_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: gss_tsig_messages.h gss_tsig_messages.cc
@echo Message files regenerated
gss_tsig_messages.h gss_tsig_messages.cc: gss_tsig_messages.mes
$(top_builddir)/src/lib/log/compiler/kea-msg-compiler $(top_srcdir)/src/hooks/d2/gss_tsig/gss_tsig_messages.mes
else
messages gss_tsig_messages.h gss_tsig_messages.cc:
@echo Messages generation disabled. Configure with --enable-generate-messages to enable it.
endif
endif

View File

@ -0,0 +1,165 @@
// Copyright (C) 2021-2022 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 GSS-TSIG Hooks Library
Welcome to Kea GSS-TSIG 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 gssTsigOverview Overview
The @c gss_tsig library provides support for GSS-TSIG.
@section gssTsigReferences References
GSS-TSIG is at the end of different protocols:
- Kerberos 5: a security protocol developed for the MIT Athena project
since 1983. The version 5 is the last one and was published in RFC 4120.
- GSS-API (Generic Security Services Application Program Interface) is
a protocol independent API even it is mainly used with Kerberos 5.
The last version of GSS-API with Kerberos 5 is specified in RFC 4121.
- The GSS-API itself is in RFC 2743 and its C bindings in RFC 2744
(C++ bindings are in the gss_tsig_api.h file).
- The protocol used to negotiate the security setup is SPNEGO
(SPNEGO Simple and Protected GSSAPI Negotiation Mechanism) described
in RFC 4178.
- The mechanism to protect DNS exchange is TSIG (RFC 2845 updated by
RFC 4635 and RFC 8945).
- The TKEY Resource Record defined in RFC 2930 transports opaque crypto
payloads between a client and a server.
- GSS-TSIG described in RFC 3645 is a reuse of TSIG using GSS-API to
setup a security context used to perform crypto signature creation and
verification.
To summary the GSS-API with Kerberos 5 using SPNEGO establishes a security
context using TKEY to transport opaque tokens between a client and a server.
When the security context is established it is used to protect DNS exchanges,
typically DNS dynamic updates using GSS-TSIG.
For this hook:
- TSIG is implemented in the DNS++ library
- TKEY was added to the DNS++ library
- Kerberos 5 and SPNEGO are not directly used
- remains the GSS-API itself (using the C Kerberos 5 API)
@section gssTsigGssApi GSS-API
There are 3 important kinds of objects in GSS-API:
- names, here Kerberos 5 files
- credentials which are crypto data associated to a name, including
for the client / initiator side a notion of default credential
- security context which allows crypto signature creation and verification.
So only important operation is to establish a security context.
This operation is different in the initiator and the responser sides
but is based on the same schema: a function (init for the initiator and
accept for the responder) is called with input and output tokens until
the returned status is "complete" vs "continue".
The output token is sent to the other side which uses it as the input token.
The first step is to provide an empty token to the init function.
@section gssTsigGssCommands Commands
The @c gss_tsig hooks library supports some commands:
- gss-tsig-list returns the ID of all configured DNS servers and the name
of all GSS-TSIG keys.
- gss-tsig-get-all returns all configured DNS servers with their GSS-TSIG keys.
- gss-tsig-get returns the configured DNS server with the given server-id.
- gss-tsig-key-get returns the GSS-TSIG key with the key-name FQDN.
- gss-tsig-key-expire changes the status of the GSS-TSIG key with the
key-name FQDN to expired.
- gss-tsig-key-del deletes the GSS-TSIG key with the key-name FQDN.
- gss-tsig-purge-all removes expired and in error GSS-TSIG keys.
- gss-tsig-purge removes expired and in error GSS-TSIG keys for the DNS
server with the given server-id.
- gss-tsig-rekey-all rekeys the GSS-TSIG keys for all configured DNS servers.
- gss-tsig-rekey rekeys the GSS-TSIG key for the DNS server with the given
server-id.
@section gssTsigInternals Library Internals
## Framework
The structure of the library is very simple and contains:
- the hook library callouts (see @ref gss_tsig_callouts.cc).
- the single instance of the hook library implementation @c GssTsigImpl (see
@ref gss_tsig_impl.h).
- the configuration container @c GssTsigCfg which stores the mapping between
the hook configured DNS servers (@c DnsServer) and the kea-dhcp-ddns (D2)
configured DNS servers (@c d2::DnsServerInfoPtr) in addition to all hook
library parameters (see @ref gss_tsig_cfg.h). For a complete list of
parameters see the GSS-TSIG ARM section.
- the implementation of the @c ManagedKey which is the GSS-TSIG TKey managed
by the hook library (see @ref managed_key.h) and extends the @c GssTsigKey
(see @ref gss_tsig_key.h) which in turn extends the @c d2::D2TsigKey.
- the implementation of the @c TKeyExchange which manages the GSS-TSIG TKey
exchange with the DNS server (see @ref tkey_exchange.h).
- the implementation of the @c TSIGContext used to sign and verify GSS-TSIG
DNS messages, which extends the @c dns::TSIGContext (see
@ref gss_tsig_context.h).
- the implementation of the GSS-API C++ wrapper using the C Kerberos 5 API:
@c GssApiBuffer, @c GssApiName, @c GssApiCred, @c GssApiSecCtx, @c GssApiOid,
@c GssApiOidSet (see @ref gss_tsig_api.h).
## Callouts
The select_key hook point is used by the D2 server to select the key for the
DNS update for the current server. If GSS-TSIG is not enforced (fallback is set
to true), then non GSS-TSIG key (TSIG Key or none) will be used, but if GSS-TSIG
is explicitly required (default: fallback is set to false), then the current
server is skipped (by setting the NEXT_STEP_SKIP flag). If the current DNS
server is not skipped, the respective TKey context (none, simple TSIG or
GSS-TSIG) will be used to sign and verify the DNS updates.
The d2_srv_configured hook point is used to set up internal io service and
validate the hooks library configuration. At this stage the mapping between the
hook configured DNS servers and the kea-dhcp-ddns (D2) configured DNS servers is
done.
The multi_threading_compatible indicate that the hook library is multi-threaded
compatible, even though the D2 server does not currently use multiple threads to
process DNS updates.
The following hook points are used for commands only: get, get_all, lists,
key_get, key_expire, key_del, purge, purge_all, rekey, rekey_all.
## The automatic key expiration and rekey process
The hook library will automatically check for expired keys after each
``rekey-interval`` seconds. It will also check if any key has been created more
than ``rekey-interval`` seconds ago so that a new key is created before the
respective one expires. The ``rekey-interval`` is recommend between 50% and 80%
of the ``tkey-lifetime`` value. If any error occurs, the process is scheduled
again after ``retry-interval`` seconds. The ``retry-interval`` must be smaller
than the ``rekey-interval`` value, and should be at most 1/3 of the difference
between ``tkey-lifetime`` and ``rekey-interval``.
The key expiration is also checked whenever a key is searched for the current
DNS server, so that no expired key is used.
@section gssTsigCpp11ChronoClock Choice of the C++11 chrono clock
This stands for all other uses of the system C++11 chrono clock in Kea but
for GSS-TSIG this choice raised some questions so the choice of the
system clock vs steady clock or high resolution clock is explained here:
- the high resolution clock has only a nice property: its high resolution
which has no interest in the GSS-TSIG particular case where a second
granularity is enough.
- the steady clock is monotonic: this should have made it the best choice
for the code itself without a stronger argument.
- the system clock is real time i.e. it provides direct conversions to
and from the C time_t type including textual representations. This is
critical for user interface so for convenience it was chosen.
@section gssTsigMTCompatibility Multi-Threading Compatibility
The @c gss_tsig hooks library is compatible with multi-threading but
can be used only by the D2 server which is not yet multi-threaded.
*/

View File

@ -0,0 +1,561 @@
// Copyright (C) 2021-2022 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 <gss_tsig_api.h>
#include <cstring>
#include <limits>
#include <sstream>
using namespace std;
namespace isc {
namespace gss_tsig {
GssApiLastError::GssApiLastError() : last_error_(0) {
}
GssApiLastError::~GssApiLastError() {
}
string
gssApiErrMsg(OM_uint32 major, OM_uint32 minor) {
ostringstream msg;
GssApiBuffer msg_major;
OM_uint32 minor_stat = 0;
OM_uint32 msg_ctx = 0;
OM_uint32 major_stat = gss_display_status(&minor_stat, major,
GSS_C_GSS_CODE, GSS_C_NULL_OID,
&msg_ctx, msg_major.getPtr());
if (major_stat != GSS_S_COMPLETE) {
// gssApiErrMsg is likely to be used in exception handles, we
// can't throw here. But at the same time we want to explain
// what was the nature of the problem, so at least we print
// something on stderr, hoping the message will get to the user.
cerr << "gss_display_status(major=" << major << ") failed with "
<< major_stat << endl;
}
msg << "GSSAPI error: Major = '";
if (!msg_major.empty()) {
msg << static_cast<char*>(msg_major.getValue());
}
if (minor != 0) {
GssApiBuffer msg_minor;
minor_stat = msg_ctx = 0;
major_stat = gss_display_status(&minor_stat, minor,
GSS_C_MECH_CODE, GSS_C_NULL_OID,
&msg_ctx, msg_minor.getPtr());
if (major_stat != GSS_S_COMPLETE) {
// gssApiErrMsg is likely to be used in exception handles, we
// can't throw here. But at the same time we want to explain
// what was the nature of the problem, so at least we print
// something on stderr, hoping the message will get to the user.
cerr << "gss_display_status(minor=" << minor << ") failed with "
<< major_stat << endl;
}
msg << "' (" << major << "), Minor = '";
if (!msg_minor.empty()) {
msg << static_cast<char*>(msg_minor.getValue());
}
msg << "' (" << minor << ").";
} else {
msg << "' (" << major << ").";
}
return (msg.str());
}
GssApiBuffer::GssApiBuffer() {
memset(&buffer_, 0, sizeof(gss_buffer_desc));
}
GssApiBuffer::GssApiBuffer(size_t length, const void* value) {
memset(&buffer_, 0, sizeof(gss_buffer_desc));
if (length > numeric_limits<uint32_t>::max()) {
isc_throw(OutOfRange, "GssApiBuffer constructor: length " << length
<< " is too large");
}
buffer_.length = length;
if (buffer_.length > 0) {
// The GSS-API uses gss_release_buffer() to get rid of the buffer.
// That function uses free(), hence we need to use malloc() to allocate.
buffer_.value = malloc(buffer_.length);
if (!buffer_.value) {
buffer_.length = 0;
isc_throw(GssApiError, "GssApiBuffer constructor failed with "
<< "'Cannot allocate memory'");
}
memmove(buffer_.value, value, buffer_.length);
}
}
GssApiBuffer::GssApiBuffer(const vector<uint8_t>& content) {
memset(&buffer_, 0, sizeof(gss_buffer_desc));
if (content.size() > numeric_limits<uint32_t>::max()) {
isc_throw(OutOfRange, "GssApiBuffer constructor: vector size " <<
content.size() << " is too large");
}
buffer_.length = content.size();
if (buffer_.length > 0) {
// The GSS-API uses gss_release_buffer() to get rid of the buffer.
// That function uses free(), hence we need to use malloc() to allocate.
buffer_.value = malloc(buffer_.length);
if (!buffer_.value) {
buffer_.length = 0;
isc_throw(GssApiError, "GssApiBuffer constructor failed with "
<< "'Cannot allocate memory'");
}
memmove(buffer_.value, &content[0], buffer_.length);
}
}
GssApiBuffer::GssApiBuffer(const string& content) {
memset(&buffer_, 0, sizeof(gss_buffer_desc));
if (content.empty()) {
return;
}
if (content.size() >= numeric_limits<uint32_t>::max()) {
isc_throw(OutOfRange, "GssApiBuffer constructor: string size "
<< content.size() << " is too large");
}
// The GSS-API uses gss_release_buffer() to get rid of the buffer.
// That function uses free(), hence we need to use malloc() to allocate.
buffer_.length = content.size();
buffer_.value = malloc(buffer_.length + 1);
if (!buffer_.value) {
buffer_.length = 0;
isc_throw(GssApiError, "GssApiBuffer constructor failed with "
<< "'Cannot allocate memory'");
}
memset(buffer_.value, 0, buffer_.length + 1);
memmove(buffer_.value, content.c_str(), buffer_.length);
}
GssApiBuffer::~GssApiBuffer() {
if (buffer_.value) {
OM_uint32 minor = 0;
OM_uint32 major = gss_release_buffer(&minor, &buffer_);
if (major != GSS_S_COMPLETE) {
cerr << "gss_release_buffer failed with " << major << endl;
}
}
}
vector<uint8_t>
GssApiBuffer::getContent() const {
vector<uint8_t> content;
content.resize(buffer_.length);
if (buffer_.length > 0) {
memmove(&content[0], buffer_.value, buffer_.length);
}
return (vector<uint8_t>(content));
}
string
GssApiBuffer::getString(bool trim) const {
if (buffer_.length == 0) {
return (string());
} else if (trim) {
return (string(static_cast<char*>(buffer_.value)));
} else {
return (string(static_cast<char*>(buffer_.value), buffer_.length));
}
}
GssApiName::GssApiName() : GssApiLastError(), name_(GSS_C_NO_NAME) {
}
GssApiName::GssApiName(const string& gname)
: GssApiLastError(), name_(GSS_C_NO_NAME) {
if (gname.size() >= numeric_limits<uint32_t>::max()) {
isc_throw(OutOfRange, "GssApiName constructor: string size "
<< gname.size() << " is too large");
}
GssApiBuffer buf(gname);
OM_uint32 minor = 0;
OM_uint32 major = gss_import_name(&minor, buf.getPtr(),
GSS_C_NO_OID, &name_);
if (major != GSS_S_COMPLETE) {
isc_throw(GssApiError, "gss_import_name failed with "
<< gssApiErrMsg(major, minor));
}
}
GssApiName::~GssApiName() {
if (name_) {
OM_uint32 minor = 0;
OM_uint32 major = gss_release_name(&minor, &name_);
if (major != GSS_S_COMPLETE) {
cerr << "gss_release_name failed with " << major << endl;
}
}
}
bool
GssApiName::compare(GssApiName& other) {
OM_uint32 minor = 0;
int ret = -1;
OM_uint32 major = gss_compare_name(&minor, name_, other.name_, &ret);
if (major != GSS_S_COMPLETE) {
setLastError(major);
isc_throw(GssApiError, "gss_compare_name failed with "
<< gssApiErrMsg(major, minor));
}
return (ret == 1);
}
string
GssApiName::toString() {
GssApiBuffer buf;
OM_uint32 minor = 0;
OM_uint32 major = gss_display_name(&minor, name_, buf.getPtr(), 0);
if (major != GSS_S_COMPLETE) {
setLastError(major);
isc_throw(GssApiError, "gss_display_name failed with "
<< gssApiErrMsg(major, minor));
}
return (buf.getString());
}
GssApiCred::GssApiCred() : GssApiLastError(), cred_(GSS_C_NO_CREDENTIAL) {
}
GssApiCred::GssApiCred(GssApiName& gname, gss_cred_usage_t cred_usage,
OM_uint32& lifetime)
: GssApiLastError(), cred_(GSS_C_NO_CREDENTIAL) {
cred_ = GSS_C_NO_CREDENTIAL;
lifetime = 0;
GssApiOidSet mech_oid_set;
OM_uint32 minor = 0;
OM_uint32 major = gss_acquire_cred(&minor, gname.get(), GSS_C_INDEFINITE,
mech_oid_set.get(), cred_usage,
&cred_, 0, &lifetime);
if (major != GSS_S_COMPLETE) {
isc_throw(GssApiError, "gss_acquire_cred failed with "
<< gssApiErrMsg(major, minor));
}
}
GssApiCred::~GssApiCred() {
if (cred_) {
OM_uint32 minor = 0;
OM_uint32 major = gss_release_cred(&minor, &cred_);
if (major != GSS_S_COMPLETE) {
cerr << "gss_release_cred failed with " << major << endl;
}
}
}
void
GssApiCred::inquire(GssApiName& name, gss_cred_usage_t& cred_usage,
OM_uint32& lifetime) {
// cred_usage 0 means GSS_C_BOTH.
lifetime = 0;
OM_uint32 minor = 0;
OM_uint32 major = gss_inquire_cred(&minor, cred_, name.getPtr(),
&lifetime, &cred_usage, 0);
if (major != GSS_S_COMPLETE) {
setLastError(major);
isc_throw(GssApiError, "gss_inquire_cred failed with "
<< gssApiErrMsg(major, minor));
}
}
GssApiSecCtx::GssApiSecCtx(gss_ctx_id_t sec_ctx)
: GssApiLastError(), sec_ctx_(sec_ctx) {
}
GssApiSecCtx::GssApiSecCtx(const vector<uint8_t>& import)
: GssApiLastError(), sec_ctx_(GSS_C_NO_CONTEXT) {
GssApiBuffer buf(import);
OM_uint32 minor = 0;
OM_uint32 major = gss_import_sec_context(&minor, buf.getPtr(), &sec_ctx_);
if (major != GSS_S_COMPLETE) {
isc_throw(GssApiError, "gss_import_sec_context failed with "
<< gssApiErrMsg(major, minor));
}
}
GssApiSecCtx::~GssApiSecCtx() {
if (sec_ctx_) {
OM_uint32 minor = 0;
OM_uint32 major = gss_delete_sec_context(&minor, &sec_ctx_, 0);
if (major != GSS_S_COMPLETE) {
cerr << "gss_delete_sec_context failed with " << major << endl;
}
}
}
vector<uint8_t>
GssApiSecCtx::serialize() {
GssApiBuffer buf;
OM_uint32 minor = 0;
OM_uint32 major = gss_export_sec_context(&minor, &sec_ctx_, buf.getPtr());
if (major != GSS_S_COMPLETE) {
setLastError(major);
isc_throw(GssApiError, "gss_export_sec_context failed with "
<< gssApiErrMsg(major, minor));
}
return (buf.getContent());
}
OM_uint32
GssApiSecCtx::getLifetime() {
OM_uint32 lifetime = 0;
OM_uint32 minor = 0;
OM_uint32 major = gss_context_time(&minor, sec_ctx_, &lifetime);
if (major != GSS_S_COMPLETE) {
setLastError(major);
isc_throw(GssApiError, "gss_context_time failed with "
<< gssApiErrMsg(major, minor));
}
return (lifetime);
}
void
GssApiSecCtx::inquire(GssApiName& source, GssApiName& target,
OM_uint32& lifetime, OM_uint32& flags,
bool& local, bool& established) {
lifetime = flags = 0;
local = established = false;
int locally_initiated = 0;
int open = 0;
OM_uint32 minor = 0;
OM_uint32 major = gss_inquire_context(&minor, sec_ctx_,
source.getPtr(), target.getPtr(),
&lifetime, 0, &flags,
&locally_initiated, &open);
if (major != GSS_S_COMPLETE) {
setLastError(major);
isc_throw(GssApiError, "gss_inquire_context failed with "
<< gssApiErrMsg(major, minor));
}
local = (locally_initiated != 0);
established = (open != 0);
}
void
GssApiSecCtx::sign(GssApiBuffer& gmessage, GssApiBuffer& gsig) {
OM_uint32 minor = 0;
OM_uint32 major = gss_get_mic(&minor, sec_ctx_, GSS_C_QOP_DEFAULT,
gmessage.getPtr(), gsig.getPtr());
if (major != GSS_S_COMPLETE) {
setLastError(major);
isc_throw(GssApiError, "gss_get_mic failed with "
<< gssApiErrMsg(major, minor));
}
}
void
GssApiSecCtx::verify(GssApiBuffer& gmessage, GssApiBuffer& gsig) {
OM_uint32 minor = 0;
OM_uint32 major = gss_verify_mic(&minor, sec_ctx_, gmessage.getPtr(),
gsig.getPtr(), 0);
if (major != GSS_S_COMPLETE) {
setLastError(major);
isc_throw(GssApiError, "gss_verify_mic failed with "
<< gssApiErrMsg(major, minor));
}
}
bool
GssApiSecCtx::init(GssApiCredPtr credp, GssApiName& target, OM_uint32 flags,
GssApiBuffer& intoken, GssApiBuffer& outtoken,
OM_uint32& lifetime) {
gss_cred_id_t cred = GSS_C_NO_CREDENTIAL;
if (credp) {
cred = credp->get();
}
lifetime = 0;
OM_uint32 ret_flags = 0;
OM_uint32 minor = 0;
OM_uint32 major = gss_init_sec_context(&minor, cred,
&sec_ctx_, target.get(),
ISC_GSS_SPNEGO_MECHANISM.get(),
flags, GSS_C_INDEFINITE,
GSS_C_NO_CHANNEL_BINDINGS,
intoken.getPtr(), 0,
outtoken.getPtr(), &ret_flags,
&lifetime);
switch (major) {
case GSS_S_COMPLETE:
if ((flags & GSS_C_REPLAY_FLAG) &&
((ret_flags & GSS_C_REPLAY_FLAG) == 0)) {
isc_throw(GssApiError, "gss_init_sec_context failed to grant "
"requested anti-replay");
}
if ((flags & GSS_C_SEQUENCE_FLAG) &&
((ret_flags & GSS_C_SEQUENCE_FLAG) == 0)) {
isc_throw(GssApiError, "gss_init_sec_context failed to grant "
"requested sequence");
}
if ((flags & GSS_C_MUTUAL_FLAG) &&
((ret_flags & GSS_C_MUTUAL_FLAG) == 0)) {
isc_throw(GssApiError, "gss_init_sec_context failed to grant "
"requested mutual authentication");
}
return (true);
case GSS_S_CONTINUE_NEEDED:
return (false);
default:
setLastError(major);
isc_throw(GssApiError, "gss_init_sec_context failed with "
<< gssApiErrMsg(major, minor));
}
}
bool
GssApiSecCtx::accept(GssApiCred& cred, GssApiBuffer& intoken,
GssApiName& source, GssApiBuffer& outtoken) {
OM_uint32 minor = 0;
OM_uint32 major = gss_accept_sec_context(&minor, &sec_ctx_, cred.get(),
intoken.getPtr(),
GSS_C_NO_CHANNEL_BINDINGS,
source.getPtr(), 0,
outtoken.getPtr(), 0, 0, 0);
switch (major) {
case GSS_S_COMPLETE:
return (true);
case GSS_S_CONTINUE_NEEDED:
return (false);
default:
setLastError(major);
isc_throw(GssApiError, "gss_accept_sec_context failed with "
<< gssApiErrMsg(major, minor));
}
}
GssApiOid::GssApiOid() : oid_(GSS_C_NO_OID) {
// The GSS-API uses gss_release_oid() to release OID buffer. That function
// uses free(), hence we need to use malloc() to allocate it.
oid_ = static_cast<gss_OID>(malloc(sizeof(gss_OID_desc)));
if (!oid_) {
isc_throw(GssApiError, "GssApiOid constructor failed with "
<< "'Cannot allocate memory' (desc)");
}
memset(oid_, 0, sizeof(gss_OID_desc));
}
GssApiOid::GssApiOid(const vector<uint8_t>& elements) : oid_(GSS_C_NO_OID) {
if (elements.size() > 1024) {
isc_throw(OutOfRange, "Too large argument to GssApiOid ("
<< elements.size() << " > 1024)");
}
// The GSS-API uses gss_release_oid() to release OID buffer. That function
// uses free(), hence we need to use malloc() to allocate it.
oid_ = static_cast<gss_OID>(malloc(sizeof(gss_OID_desc)));
if (!oid_) {
isc_throw(GssApiError, "GssApiOid constructor failed with "
<< "'Cannot allocate memory' (desc)");
}
memset(oid_, 0, sizeof(gss_OID_desc));
oid_->length = elements.size();
if (oid_->length > 0) {
// The GSS-API uses gss_release_oid_set() to release OID buffer.
// That function uses free(), hence we need to use malloc() to allocate.
oid_->elements = malloc(oid_->length);
if (!oid_->elements) {
oid_->length = 0;
isc_throw(GssApiError, "GssApiOid constructor failed with "
<< "'Cannot allocate memory' (elements)");
}
memmove(oid_->elements, &elements[0], oid_->length);
}
}
GssApiOid::GssApiOid(const string& str) : oid_(GSS_C_NO_OID) {
#if HAVE_GSS_STR_TO_OID
GssApiBuffer buf(str);
OM_uint32 minor = 0;
OM_uint32 major = gss_str_to_oid(&minor, buf.getPtr(), &oid_);
if (major != GSS_S_COMPLETE) {
isc_throw(GssApiError, "gss_str_to_oid failed with "
<< gssApiErrMsg(major, minor));
}
#else
isc_throw(NotImplemented, "gss_str_to_oid(" << str << ") is not available");
#endif
}
GssApiOid::~GssApiOid() {
if (oid_) {
OM_uint32 minor = 0;
OM_uint32 major = gss_release_oid(&minor, &oid_);
if (major != GSS_S_COMPLETE) {
cerr << "gss_release_oid failed with " << major << endl;
}
}
}
string
GssApiOid::toString() {
GssApiBuffer buf;
OM_uint32 minor = 0;
OM_uint32 major = gss_oid_to_str(&minor, oid_, buf.getPtr());
if (major != GSS_S_COMPLETE) {
isc_throw(GssApiError, "gss_oid_to_str failed with "
<< gssApiErrMsg(major, minor));
}
return (buf.getString(true));
}
namespace {
vector<uint8_t> ISC_GSS_KRB5_MECHANISM_vect =
{ 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x12, 0x01, 0x02, 0x02 };
}
GssApiOid ISC_GSS_KRB5_MECHANISM(ISC_GSS_KRB5_MECHANISM_vect);
namespace {
vector<uint8_t> ISC_GSS_SPNEGO_MECHANISM_vect =
{ 0x2b, 0x06, 0x01, 0x05, 0x05, 0x02 };
}
GssApiOid ISC_GSS_SPNEGO_MECHANISM(ISC_GSS_SPNEGO_MECHANISM_vect);
GssApiOidSet::GssApiOidSet(bool fill) {
oid_set_ = GSS_C_NO_OID_SET;
if (!fill) {
return;
}
OM_uint32 minor = 0;
OM_uint32 major = gss_create_empty_oid_set(&minor, &oid_set_);
if (major != GSS_S_COMPLETE) {
isc_throw(GssApiError, "gss_create_empty_oid_set failed with "
<< gssApiErrMsg(major, minor));
}
minor = 0;
major = gss_add_oid_set_member(&minor, ISC_GSS_KRB5_MECHANISM.get(),
&oid_set_);
if (major != GSS_S_COMPLETE) {
isc_throw(GssApiError, "gss_add_oid_set_member(KRB5) failed with "
<< gssApiErrMsg(major, minor));
}
minor = 0;
major = gss_add_oid_set_member(&minor, ISC_GSS_SPNEGO_MECHANISM.get(),
&oid_set_);
if (major != GSS_S_COMPLETE) {
isc_throw(GssApiError, "gss_add_oid_set_member(SPNEGO) failed with "
<< gssApiErrMsg(major, minor));
}
}
GssApiOidSet::~GssApiOidSet() {
if (oid_set_) {
OM_uint32 minor = 0;
OM_uint32 major = gss_release_oid_set(&minor, &oid_set_);
if (major != GSS_S_COMPLETE) {
cerr << "gss_release_oid_set failed with " << major << endl;
}
}
}
} // end of namespace isc::gss_tsig
} // end of namespace isc

View File

@ -0,0 +1,498 @@
// Copyright (C) 2021-2022 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 gss_tsig_api.h C++ binding for the GSS-API.
///
/// This files provide C++ bindings: each type has its corresponding
/// C++ class: when a type is a structure (descriptor in GSS-API term)
/// it gets a class encapsulating the structure at the exception of
/// ASN.1 Object IDs. Some types are pointers to opaque structures.
///
/// To avoid ownership or memory issues all classes are not copyable
/// and should be passed by references. Note the GSS-API does not make
/// any argument const at a few exceptions so these references are
/// plain (vs const) references.
///
/// The central GSS-API structure is the buffer. Opaque structures so
/// pointer types are names, credential and security contexts.
///
/// Recommended reading:
/// - RFC 2743 (GSS-API protocol) or
/// - RFC 2744 (GSS-API - C Bindings)
#ifndef GSS_TSIG_UTIL_H
#define GSS_TSIG_UTIL_H
#include <exceptions/exceptions.h>
#include <boost/noncopyable.hpp>
#include <boost/shared_ptr.hpp>
#include <gssapi/gssapi_krb5.h>
#include <iostream>
#include <string>
#include <vector>
namespace isc {
namespace gss_tsig {
/// @brief GSS-API exception.
class GssApiError : public Exception {
public:
GssApiError(const char* file, size_t line, const char* what) :
isc::Exception(file, line, what) {
}
};
class GssCredExpired : public Exception {
public:
GssCredExpired(const char* file, size_t line, const char* what) :
isc::Exception(file, line, what) {
}
};
/// @brief Last error base class.
///
/// This class caches the last error.
class GssApiLastError {
public:
/// @brief Constructor.
GssApiLastError();
/// @brief Destructor.
virtual ~GssApiLastError();
/// @brief Get the last error.
///
/// @return The last error.
int getLastError() const {
return (last_error_);
}
/// @brief Set the last error.
///
/// @param error New error.
void setLastError(int error) {
last_error_ = error;
}
private:
/// @brief The last error.
int last_error_;
};
/// @brief An the error message.
///
/// Use the gss_display_status GSS-API function.
///
/// @param major Major error code (GSS).
/// @param minor Minor error code (Mech).
/// @return The corresponding error message.
std::string gssApiErrMsg(OM_uint32 major, OM_uint32 minor);
/// @brief GSS-API buffer.
///
/// Encapsulate the gss_buffer_desc (structure with length and value) type.
///
/// @note: the memory is not shared by constructor and get content methods,
/// i.e. a copy is taken and the source can be destroyed at any time after.
class GssApiBuffer : public boost::noncopyable {
public:
/// @brief Constructor.
GssApiBuffer();
/// @brief Constructor.
///
/// @param length Buffer length.
/// @param value Buffer value.
GssApiBuffer(size_t length, const void* value);
/// @brief Constructor.
///
/// @param content Content as a vector of bytes.
explicit GssApiBuffer(const std::vector<uint8_t>& content);
/// @brief Constructor.
///
/// @param content Content as a string.
explicit GssApiBuffer(const std::string& content);
/// @brief Destructor.
///
/// Use gss_release_buffer GSS-API function.
~GssApiBuffer();
/// @brief Empty predicate.
///
/// @return true if empty, false is not empty.
bool empty() const {
return (buffer_.value == 0);
}
/// @brief Get pointer.
///
/// @return a pointer to the buffer.
gss_buffer_t getPtr() {
return (&buffer_);
}
/// @brief Get the length.
///
/// @return the length.
size_t getLength() const {
return (buffer_.length);
}
/// @brief Get the value.
///
/// @note: please use this method only to copy the content:
/// the value lifetime is the same as the object.
///
/// @return the value.
void* getValue() {
return (buffer_.value);
}
/// @brief Get the content as a vector.
///
/// @return the content as a vector of bytes.
std::vector<uint8_t> getContent() const;
/// @brief Get the content as a string.
///
/// @note It is not allowed to overload with a different return type
/// so the getContent method name is not available.
/// @note: This is not the same as string(getValue()) because of
/// the way the nul character is handled in C++ strings.
///
/// @param trim When true treat the buffer as a C string, when false
/// (the default) treat the buffer as a C++ string.
/// @return the content as a string.
std::string getString(bool trim = false) const;
private:
/// @brief The GSS-API buffer.
gss_buffer_desc buffer_;
};
/// @brief Shared pointer to GSS-API buffer.
typedef boost::shared_ptr<GssApiBuffer> GssApiBufferPtr;
/// @brief GSS-API name.
///
/// Encapsulate the gss_name_t GSS-API pointer type.
/// @note: some methods should be const but this does not match the API.
class GssApiName : public boost::noncopyable, public GssApiLastError {
public:
/// @brief Constructor.
GssApiName();
/// @brief Constructor.
///
/// @param gname The GSS-API name in a textual form.
explicit GssApiName(const std::string& gname);
/// @brief Destructor.
///
/// Use gss_release_name GSS-API function.
~GssApiName();
/// @brief Get the value.
gss_name_t get() {
return (name_);
}
/// @brief Get pointer.
///
/// @return a pointer to the name.
gss_name_t* getPtr() {
return (&name_);
}
/// @brief Compare.
///
/// Use the gss_compare_name GSS-API funtion.
///
/// @param other Other GSS-API name.
/// @return true if identical, false if different.
bool compare(GssApiName& other);
/// @brief textual representation.
///
/// Use the gss_display_name GSS-API funtion.
///
/// @return a string representing the GSS-API name.
std::string toString();
private:
/// @brief The GSS-API name.
gss_name_t name_;
};
/// @brief Shared pointer to GSS-API name.
typedef boost::shared_ptr<GssApiName> GssApiNamePtr;
/// @brief GSS-API credential.
///
/// Encapsulate the gss_cred_id_t GSS-API pointer type.
///
/// @note: some methods should be const but this does not match the API.
class GssApiCred : public boost::noncopyable, public GssApiLastError {
public:
/// @brief Constructor.
GssApiCred();
/// @brief Constructor.
///
/// Use the gss_acquire_cred GSS-API function.
///
/// @param gname Desired GSS-API name.
/// @param cred_usage Credential usage (GSS_C_INITIATE or GSS_C_ACCEPT).
/// @param[out] lifetime Validity lifetime (number of seconds from now).
GssApiCred(GssApiName& gname, gss_cred_usage_t cred_usage,
OM_uint32& lifetime);
/// @brief Destructor.
///
/// Use the gss_release_cred GSS-API function
~GssApiCred();
/// @brief Get the value.
gss_cred_id_t get() {
return (cred_);
}
/// @brief Inquire.
///
/// Use the gss_inquire_cred GSS-API function.
///
/// @param[out] name GSS-API name.
/// @param[out] cred_usage Credential usage.
/// @param[out] lifetime Validity lifetime (number of seconds from now).
void inquire(GssApiName& name, gss_cred_usage_t& cred_usage,
OM_uint32& lifetime);
private:
/// @brief The GSS-API credential.
gss_cred_id_t cred_;
};
/// @brief Shared pointer to GSS-API credential.
typedef boost::shared_ptr<GssApiCred> GssApiCredPtr;
/// @brief GSS-API security context.
///
/// Encapsulate gss_ctx_id_t the GSS-API pointer type.
///
/// @note: some methods should be const but this does not match the API.
class GssApiSecCtx : public boost::noncopyable, public GssApiLastError {
public:
/// @brief Constructor.
///
/// @param sec_ctx The GSS-API security context.
explicit GssApiSecCtx(gss_ctx_id_t sec_ctx);
/// @brief Import constructor.
///
/// Use the gss_import_sec_context GSS-API function.
///
/// @param import Vector of byte representing the GSS-API security context.
explicit GssApiSecCtx(const std::vector<uint8_t>& import);
/// @brief Destructor.
///
/// Use the gss_delete_sec_context GSS-API function.
~GssApiSecCtx();
/// @brief Get the value.
gss_ctx_id_t get() {
return (sec_ctx_);
}
/// @brief Get a pointer to the security context.
///
/// @return a pointer to the security context.
gss_ctx_id_t* getPtr() {
return (&sec_ctx_);
}
/// @brief Export.
///
/// Use the gss_export_sec_context GSS-API function.
///
/// @return A vector of byte representing the GSS-API security context.
std::vector<uint8_t> serialize();
/// @brief Get the lifetime (validity in seconds).
///
/// Use the gss_context_time GSS-API function.
///
OM_uint32 getLifetime();
/// @brief Inquire.
///
/// Use the gss_inquire_context GSS-API function.
///
/// @param[out] source Source GSS-API name.
/// @param[out] target Target GSS-API name.
/// @param[out] lifetime Validity lifetime (number of seconds from now).
/// @param[out] flags Current flags.
/// @param[out] local True when locally initialed, false otherwise.
/// @param[out] established True when established, false when a token is
/// still expected.
void inquire(GssApiName& source, GssApiName& target, OM_uint32& lifetime,
OM_uint32& flags, bool& local, bool& established);
/// @brief Sign.
///
/// Use the gss_get_mic GSS-API function.
///
/// @param gmessage GSS-API buffer containing the message to sign.
/// @param[out] gsig GSS-API buffer to handle the signature.
void sign(GssApiBuffer& gmessage, GssApiBuffer& gsig);
/// @brief Verify.
///
/// Use the gss_verify_mic GSS-API function.
///
/// @param gmessage GSS-API buffer containing the message to verify.
/// @param gsig GSS-API buffer containing the signature to verify.
void verify(GssApiBuffer& gmessage, GssApiBuffer& gsig);
/// @brief Init.
///
/// Init is the client setup method: it should be called
/// until it completes. The input and output tokens are used
/// for communication with the peer i.e. the acceptor.
///
/// Use the gss_init_sec_context GSS-API function.
///
/// @param credp Pointer to claimant GSS-API credential.
/// @param target Target GSS-API name.
/// @param flags Requested flags.
/// @param intoken Input token (a GSS-API buffer).
/// @param[out] outtoken Output token (a GSS-API buffer).
/// @param[out] lifetime Validity lifetime (number of seconds from now).
/// @return True when complete, false when continue (i.e. must be called
/// again).
bool init(GssApiCredPtr credp, GssApiName& target, OM_uint32 flags,
GssApiBuffer& intoken, GssApiBuffer& outtoken,
OM_uint32& lifetime);
/// @brief Accept.
///
/// Accept is the server acceptor method: it should be called
/// until it completes. The input and output tokens are used
/// for communication with the peer i.e. a client.
///
/// Use the gss_accept_sec_context GSS-API function.
///
/// @param cred Acceptor GSS-API credential.
/// @param intoken Input token (a GSS-API buffer).
/// @param[out] source Source GSS-API name.
/// @param[out] outtoken Output token (a GSS-API buffer).
/// @return True when complete, false when continue (i.e. must be called
/// again).
bool accept(GssApiCred& cred, GssApiBuffer& intoken, GssApiName& source,
GssApiBuffer& outtoken);
private:
/// @brief The GSS-API security context.
gss_ctx_id_t sec_ctx_;
};
/// @brief GSS-API OID.
///
/// Encapsulate the gss_OID GSS-API pointer type.
///
/// @note: gss_release_oid() releases the descriptor too so this class
/// encapsulate the gss_OID pointer type (vs the gss_OID_desc
/// (structure with length and elements) type.
///
class GssApiOid : public boost::noncopyable {
public:
/// @brief Constructor.
GssApiOid();
/// @brief Constructor.
///
/// @param elements Elements as a vector of bytes.
explicit GssApiOid(const std::vector<uint8_t>& elements);
/// @brief Constructor.
///
/// Use the gss_str_to_oid GSS-API function.
///
/// @note: is not available on Heimdal.
///
/// @param str Textual representation.
explicit GssApiOid(const std::string& str);
/// @brief Destructor.
///
/// Use the gss_release_oid GSS-API function.
~GssApiOid();
/// @brief Get the value.
gss_OID get() {
return (oid_);
}
/// @brief Get textual representation.
///
/// Use the gss_oid_to_str GSS-API function.
///
/// @return A textual representation.
std::string toString();
private:
/// @brief The GSS-API OID.
gss_OID oid_;
};
/// @brief The Kerberos 5 OID.
extern GssApiOid ISC_GSS_KRB5_MECHANISM;
/// @brief The SPNEGO OID.
extern GssApiOid ISC_GSS_SPNEGO_MECHANISM;
/// @brief Shared pointer to GSS-API OID.
typedef boost::shared_ptr<GssApiOid> GssApiOidPtr;
/// @brief GSS-API OID set.
///
/// Encapsulate the gss_OID_set pointer type.
///
/// @note: no need yet to give access to the whole C API.
class GssApiOidSet : public boost::noncopyable {
public:
/// @brief Constructor.
///
/// @param fill True if the set must be filled with Kerberos 5 and
/// SPNEGO OIDs (the default), false if it must be left null.
explicit GssApiOidSet(bool fill = true);
/// @brief Destructor.
///
/// Use the gss_release_oid_set GSS-API function.
~GssApiOidSet();
/// @brief Get the value.
gss_OID_set get() {
return (oid_set_);
}
private:
/// @brief The GSS-API OID set.
gss_OID_set oid_set_;
};
/// @brief Shared pointer to GSS-API OID set.
typedef boost::shared_ptr<GssApiOidSet> GssApiOidSetPtr;
} // end of namespace isc::gss_tsig
} // end of namespace isc
#endif // GSS_TSIG_UTIL_H

View File

@ -0,0 +1,297 @@
// Copyright (C) 2021-2024 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 <gss_tsig_context.h>
#include <gss_tsig_impl.h>
#include <gss_tsig_log.h>
#include <process/daemon.h>
#include <exceptions/exceptions.h>
#include <functional>
#include <sstream>
#include <string>
using namespace isc;
using namespace isc::asiolink;
using namespace isc::gss_tsig;
using namespace isc::d2;
using namespace isc::data;
using namespace isc::dns;
using namespace isc::hooks;
using namespace isc::log;
using namespace isc::process;
using namespace std;
namespace isc {
namespace gss_tsig {
/// @brief The GSS-TSIG hook implementation object.
GssTsigImplPtr impl;
} // end of namespace isc::gss_tsig
} // end of namespace isc
extern "C" {
/// @brief The gss-tsig-get command.
///
/// This command takes an argument id with a string value.
///
/// @param handle CalloutHandle.
/// @return always 0.
int get(CalloutHandle& handle) {
impl->getHandler(handle);
return (0);
}
/// @brief The gss-tsig-get-all command.
///
/// @param handle CalloutHandle.
/// @return always 0.
int get_all(CalloutHandle& handle) {
impl->getAllHandler(handle);
return (0);
}
/// @brief The gss-tsig-list command.
///
/// @param handle CalloutHandle.
/// @return always 0.
int lists(CalloutHandle& handle) {
impl->listHandler(handle);
return (0);
}
/// @brief The gss-tsig-key-get command.
///
/// This command takes an argument name with a string value.
///
/// @param handle CalloutHandle.
/// @return always 0.
int key_get(CalloutHandle& handle) {
impl->keyGetHandler(handle);
return (0);
}
/// @brief The gss-tsig-key-expire command.
///
/// This command takes an argument name with a string value.
///
/// @param handle CalloutHandle.
/// @return always 0.
int key_expire(CalloutHandle& handle) {
impl->keyExpireHandler(handle);
return (0);
}
/// @brief The gss-tsig-key-del command.
///
/// This command takes an argument name with a string value.
///
/// @param handle CalloutHandle.
/// @return always 0.
int key_del(CalloutHandle& handle) {
impl->keyDelHandler(handle);
return (0);
}
/// @brief The gss-tsig-purge command.
///
/// This command takes an argument id with a string value.
///
/// @param handle CalloutHandle.
/// @return always 0.
int purge(CalloutHandle& handle) {
impl->purgeHandler(handle);
return (0);
}
/// @brief The gss-tsig-purge-all command.
///
/// @param handle CalloutHandle.
/// @return always 0.
int purge_all(CalloutHandle& handle) {
impl->purgeAllHandler(handle);
return (0);
}
/// @brief The gss-tsig-rekey command.
///
/// This command takes an argument id with a string value.
///
/// @param handle CalloutHandle.
/// @return always 0.
int rekey(CalloutHandle& handle) {
impl->rekeyHandler(handle);
return (0);
}
/// @brief The gss-tsig-rekey-all command.
///
/// @param handle CalloutHandle.
/// @return always 0.
int rekey_all(CalloutHandle& handle) {
impl->rekeyAllHandler(handle);
return (0);
}
/// @brief This function is called when the library is loaded.
///
/// @return always 0.
int load(LibraryHandle& handle) {
try {
// Create the implementation object.
impl.reset(new GssTsigImpl());
// Make the hook library loadable only by d2.
const std::string& proc_name = Daemon::getProcName();
if (proc_name != "kea-dhcp-ddns") {
isc_throw(Unexpected, "Bad process name: " << proc_name
<< ", expected kea-dhcp-ddns");
}
// Load the configuration (syntax check).
ConstElementPtr config = handle.getParameters();
impl->configure(config);
// Register commands.
handle.registerCommandCallout("gss-tsig-get", get);
handle.registerCommandCallout("gss-tsig-get-all", get_all);
handle.registerCommandCallout("gss-tsig-key-del", key_del);
handle.registerCommandCallout("gss-tsig-key-expire", key_expire);
handle.registerCommandCallout("gss-tsig-key-get", key_get);
handle.registerCommandCallout("gss-tsig-list", lists);
handle.registerCommandCallout("gss-tsig-purge", purge);
handle.registerCommandCallout("gss-tsig-purge-all", purge_all);
handle.registerCommandCallout("gss-tsig-rekey", rekey);
handle.registerCommandCallout("gss-tsig-rekey-all", rekey_all);
} catch (const std::exception& ex) {
LOG_ERROR(gss_tsig_logger, GSS_TSIG_LOAD_FAILED)
.arg(ex.what());
return (1);
}
LOG_INFO(gss_tsig_logger, GSS_TSIG_LOAD_OK);
return (0);
}
/// @brief This function is called when the library is unloaded.
///
/// @return always 0.
int unload() {
if (impl) {
IOServiceMgr::instance().unregisterIOService(impl->getIOService());
impl->stop();
impl.reset();
}
LOG_INFO(gss_tsig_logger, GSS_TSIG_UNLOAD_OK);
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);
}
/// This function is called when the server finishes (re)configuration.
///
/// The server reverse map is built and an error is returned when a
/// configuration mismatch is detected.
///
/// @param handle CalloutHandle.
/// @return always 0.
/// @throw Unexpected when something went really wrong.
int d2_srv_configured(CalloutHandle& handle) {
// First check the status.
if (handle.getStatus() != CalloutHandle::NEXT_STEP_CONTINUE) {
return (0);
}
IOServiceMgr::instance().registerIOService(impl->getIOService());
D2CfgContextPtr d2_config;
// Get the parameters.
handle.getArgument("server_config", d2_config);
if (!d2_config) {
const string error("Error: gss_tsig d2_srv_configured: server_config is null");
handle.setArgument("error", error);
handle.setStatus(isc::hooks::CalloutHandle::NEXT_STEP_DROP);
return (1);
}
try {
impl->finishConfigure(d2_config);
impl->getIOService()->post([]() { impl->start(); });
} catch (const std::exception& ex) {
ostringstream os;
os << "gss_tsig config mismatch: " << ex.what();
string error(os.str());
handle.setArgument("error", error);
handle.setStatus(CalloutHandle::NEXT_STEP_DROP);
return (1);
}
return (0);
}
/// This function is called when the server selects a DNS server and
/// optionally a TSIG key.
///
/// An usable GSS-TSIG key for the DNS server is looked for:
/// - if GSS-TSIG is not enabled: just return with CONTINUE
/// - if a key was found: overwrite the selected key with it and
/// return with CONTINUE
/// - if no key was found but GSS-TSIG is enabled: return with SKIP
/// (i.e. implement this as GSS-TSIG is required)
///
/// @param handle CalloutHandle.
/// @return always 0.
int select_key(CalloutHandle& handle) {
// First check the status.
if (handle.getStatus() != CalloutHandle::NEXT_STEP_CONTINUE) {
return (0);
}
// Get the parameters.
DnsServerInfoPtr server_info;
handle.getArgument("current_server", server_info);
D2TsigKeyPtr tsig_key;
handle.getArgument("tsig_key", tsig_key);
// Get the DNS server.
D2TsigKeyPtr key;
bool useGssTsig = false;
bool fallback = false;
if (server_info) {
key = impl->findKey(server_info, useGssTsig, fallback);
}
if (useGssTsig) {
if (key) {
handle.setArgument("tsig_key", key);
} else if (!fallback) {
handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);
}
}
return (0);
}
/// @brief This function is called when a command was processed.
///
/// Adds an entry to status-get answer.
///
/// @param handle CalloutHandle.
/// @return always 0.
int command_processed(CalloutHandle& handle) {
try {
impl->commandProcessed(handle);
} catch (const std::exception& ex) {
LOG_ERROR(gss_tsig_logger, GSS_TSIG_COMMAND_PROCESSED_FAILED)
.arg(ex.what());
return (1);
}
return (0);
}
} // end extern "C"

View File

@ -0,0 +1,718 @@
// Copyright (C) 2021-2024 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 <dns/name.h>
#include <gss_tsig_cfg.h>
#include <stats/stats_mgr.h>
#include <limits>
using namespace isc::asiodns;
using namespace isc::asiolink;
using namespace isc::d2;
using namespace isc::data;
using namespace isc::dhcp;
using namespace isc::stats;
using namespace std;
namespace isc {
namespace gss_tsig {
const SimpleKeywords DnsServer::SERVER_PARAMETERS = {
{ "id", Element::string },
{ "domain-names", Element::list },
{ "ip-address", Element::string },
{ "port", Element::integer },
{ "server-principal", Element::string },
{ "client-principal", Element::string },
{ "gss-replay-flag", Element::boolean },
{ "gss-sequence-flag", Element::boolean },
{ "tkey-lifetime", Element::integer },
{ "rekey-interval", Element::integer },
{ "retry-interval", Element::integer },
{ "tkey-protocol", Element::string },
{ "fallback", Element::boolean },
{ "exchange-timeout", Element::integer },
{ "user-context", Element::map },
{ "comment", Element::string }
};
const list<string> DnsServer::STAT_NAMES = {
"gss-tsig-key-created",
"tkey-sent",
"tkey-success",
"tkey-timeout",
"tkey-error"
};
DnsServer::DnsServer(const string& id, const set<string>& domains,
const IOAddress& ip_address, uint16_t port)
: id_(id), domains_(domains), ip_address_(ip_address), port_(port),
server_infos_(), server_principal_(""), key_name_suffix_(""),
cred_principal_(""), gss_replay_flag_(true),
gss_sequence_flag_(false), tkey_lifetime_(DEFAULT_KEY_LIFETIME),
rekey_interval_(DEFAULT_REKEY_INTERVAL),
retry_interval_(DEFAULT_RETRY_INTERVAL), tkey_proto_(IOFetch::TCP),
fallback_(false), exchange_timeout_(DEFAULT_EXCHANGE_TIMEOUT), timer_() {
BOOST_STATIC_ASSERT(DEFAULT_REKEY_INTERVAL < DEFAULT_KEY_LIFETIME);
BOOST_STATIC_ASSERT(DEFAULT_RETRY_INTERVAL < DEFAULT_REKEY_INTERVAL);
initStats();
}
DnsServer::~DnsServer() {
removeStats();
}
void
DnsServer::initStats() {
StatsMgr& stats_mgr = StatsMgr::instance();
for (auto const& name : DnsServer::STAT_NAMES) {
const string& sname = StatsMgr::generateName("server", id_, name);
stats_mgr.setValue(sname, static_cast<int64_t>(0));
}
}
void
DnsServer::removeStats() {
StatsMgr& stats_mgr = StatsMgr::instance();
for (auto const& name : DnsServer::STAT_NAMES) {
const string& sname = StatsMgr::generateName("server", id_, name);
stats_mgr.del(sname);
}
}
void
DnsServer::resetStats() {
StatsMgr& stats_mgr = StatsMgr::instance();
for (auto const& name : DnsServer::STAT_NAMES) {
const string& sname = StatsMgr::generateName("server", id_, name);
stats_mgr.reset(sname);
}
}
void
DnsServer::buildKeyNameSuffix() {
string suffix = server_principal_;
size_t pos = suffix.find_first_of("/");
if (pos != string::npos) {
suffix = suffix.substr(pos + 1);
}
pos = suffix.find_last_of("@");
if (pos != string::npos) {
suffix = suffix.substr(0, pos);
}
if (suffix.empty()) {
isc_throw(BadValue, "can't get the GSS-TSIG key name suffix from "
<< "the DNS server principal '" << server_principal_
<< "'");
}
key_name_suffix_ = string("sig-") + suffix;
checkKeyNameSuffix();
}
void
DnsServer::checkKeyNameSuffix() {
// 32 bits mean at most 10 digits
string tname = "1234567890." + key_name_suffix_;
try {
dns::Name dname(tname);
string nname = dname.toText();
size_t pos = nname.find_first_of(".");
if (pos != 10) {
isc_throw(Unexpected, "string to FQDN failed (dot at "
<< pos << " instead 10)");
}
key_name_suffix_ = nname.substr(pos + 1);
} catch (const std::exception& ex) {
isc_throw(BadValue, "check of the GSS-TSIG key name suffix '"
<< key_name_suffix_ << "' failed: " << ex.what());
}
}
void
DnsServer::buildServerInfo(isc::d2::D2CfgContextPtr d2_config) {
if (!d2_config) {
isc_throw(D2CfgError, "empty D2 config");
}
if (!server_infos_.empty()) {
isc_throw(D2CfgError, "server info list is not empty");
}
set<string> seen;
DdnsDomainListMgrPtr d2_dom_mgr = d2_config->getForwardMgr();
DdnsDomainMapPtr d2_dom_map;
if (d2_dom_mgr) {
d2_dom_map = d2_dom_mgr->getDomains();
}
if (d2_dom_map) {
for (auto const& it : *d2_dom_map) {
if (!domains_.empty()) {
if (domains_.count(it.first) == 0) {
continue;
}
static_cast<void>(seen.insert(it.first));
}
buildServerInfo(it.second);
}
}
d2_dom_mgr = d2_config->getReverseMgr();
if (d2_dom_mgr) {
d2_dom_map = d2_dom_mgr->getDomains();
} else {
d2_dom_map = DdnsDomainMapPtr();
}
if (d2_dom_map) {
for (auto const& it : *d2_dom_map) {
if (!domains_.empty()) {
if (domains_.count(it.first) == 0) {
continue;
}
static_cast<void>(seen.insert(it.first));
}
buildServerInfo(it.second);
}
}
if (getServerInfos().empty()) {
isc_throw(NotFound, "server info can't be found");
}
if (!domains_.empty()) {
for (auto const& domain : domains_) {
if (seen.count(domain) == 0) {
isc_throw(NotFound, "domain '" << domain << "' can't be found");
}
}
}
}
void
DnsServer::buildServerInfo(isc::d2::DdnsDomainPtr d2_dns_domain) {
if (!d2_dns_domain) {
return;
}
DnsServerInfoStoragePtr servers = d2_dns_domain->getServers();
if (!servers) {
return;
}
for (auto const& info : *servers) {
if (!info) {
continue;
}
if (!info->isEnabled()) {
continue;
}
if (info->getIpAddress() != getIpAddress()) {
continue;
}
if (info->getPort() != getPort()) {
continue;
}
addServerInfo(info);
}
}
ElementPtr
DnsServer::toElement() const {
ElementPtr map = Element::createMap();
// Add user-context.
contextToElement(map);
// ID..
map->set("id", Element::create(getID()));
// Domains.
if (!domains_.empty()) {
ElementPtr domains = Element::createList();
for (auto const& domain : domains_) {
domains->add(Element::create(domain));
}
map->set("domain-names", domains);
}
// IP address.
map->set("ip-address", Element::create(ip_address_.toText()));
// Port.
map->set("port", Element::create(static_cast<int>(port_)));
// Server principal.
map->set("server-principal", Element::create(server_principal_));
// GSS-TSIG key name suffix.
map->set("key-name-suffix", Element::create(key_name_suffix_));
// Client principal.
if (!cred_principal_.empty()) {
map->set("client-principal", Element::create(cred_principal_));
}
// GSS (anti) replay flag.
map->set("gss-replay-flag", Element::create(gss_replay_flag_));
// GSS sequence flag.
map->set("gss-sequence-flag", Element::create(gss_sequence_flag_));
// TKEY lifetime.
map->set("tkey-lifetime",
Element::create(static_cast<long long>(tkey_lifetime_)));
// Rekey interval.
map->set("rekey-interval",
Element::create(static_cast<long long>(rekey_interval_)));
// Retry interval.
map->set("retry-interval",
Element::create(static_cast<long long>(retry_interval_)));
// TKEY protocol.
string proto = (tkey_proto_ == IOFetch::TCP ? "TCP" : "UDP");
map->set("tkey-protocol", Element::create(proto));
// Fallback.
map->set("fallback", Element::create(fallback_));
// TKEY exchange timeout.
map->set("exchange-timeout",
Element::create(static_cast<long long>(exchange_timeout_)));
return (map);
}
const SimpleKeywords GssTsigCfg::GLOBAL_PARAMETERS = {
{ "server-principal", Element::string },
{ "client-principal", Element::string },
{ "client-keytab", Element::string },
{ "credentials-cache", Element::string },
{ "gss-replay-flag", Element::boolean },
{ "gss-sequence-flag", Element::boolean },
{ "tkey-lifetime", Element::integer },
{ "rekey-interval", Element::integer },
{ "retry-interval", Element::integer },
{ "tkey-protocol", Element::string },
{ "fallback", Element::boolean },
{ "servers", Element::list },
{ "user-context", Element::map },
{ "comment", Element::string }
};
GssTsigCfg::GssTsigCfg()
: servers_(), servers_rev_map_(), client_keytab_(""), creds_cache_(""),
max_tkey_lifetime_(0) {
}
GssTsigCfg::~GssTsigCfg() {
}
DnsServerPtr
GssTsigCfg::getServer(const isc::d2::DnsServerInfoPtr& server_info) const {
auto candidate = servers_rev_map_.find(server_info);
if (candidate == servers_rev_map_.end()) {
return (DnsServerPtr());
}
return (candidate->second);
}
DnsServerPtr
GssTsigCfg::getServer(const string& id) const {
auto const& index = servers_.template get<DnsServerIdTag>();
auto const it = index.find(id);
if (it == index.cend()) {
return (DnsServerPtr());
}
return (*it);
}
void
GssTsigCfg::buildServerRevMap(D2CfgContextPtr d2_config) {
if (!servers_rev_map_.empty()) {
isc_throw(D2CfgError, "server reverse map is not empty");
}
for (auto const& server : getServerList()) {
server->buildServerInfo(d2_config);
for (auto const& info : server->getServerInfos()) {
if (servers_rev_map_.count(info) > 0) {
isc_throw(D2CfgError, "duplicate");
}
servers_rev_map_[info] = server;
}
}
}
void
GssTsigCfg::configure(ConstElementPtr params) {
if (!params) {
isc_throw(BadValue, "gss_tsig parameters entry is mandatory");
}
if (params->getType() != Element::map) {
isc_throw(BadValue, "gss_tsig parameters entry must be a map");
}
try {
SimpleParser::checkKeywords(GLOBAL_PARAMETERS, params);
} catch(const DhcpConfigError& ex) {
isc_throw(BadValue, "gss_tsig " << ex.what() << " ("
<< params->getPosition() << ")");
}
ConstElementPtr client_keytab = params->get("client-keytab");
if (client_keytab) {
setClientKeyTab(client_keytab->stringValue());
}
ConstElementPtr credentials_cache = params->get("credentials-cache");
if (credentials_cache) {
setCredsCache(credentials_cache->stringValue());
}
string retry_interval_origin = "default";
string retry_interval_location = "";
int64_t global_retry_val = DnsServer::DEFAULT_RETRY_INTERVAL;
ConstElementPtr global_retry_interval = params->get("retry-interval");
if (global_retry_interval) {
retry_interval_origin = "global";
retry_interval_location += " (";
retry_interval_location += global_retry_interval->getPosition().str();
retry_interval_location += ")";
global_retry_val = global_retry_interval->intValue();
if ((global_retry_val < 0) ||
(global_retry_val > numeric_limits<uint32_t>::max())) {
isc_throw(BadValue, "'retry-interval' parameter is out of "
"range [0.." << numeric_limits<uint32_t>::max()
<< "]" << retry_interval_location);
}
}
string rekey_interval_origin = "default";
string rekey_interval_location = "";
int64_t global_rekey_val = DnsServer::DEFAULT_REKEY_INTERVAL;
ConstElementPtr global_rekey_interval = params->get("rekey-interval");
if (global_rekey_interval) {
rekey_interval_origin = "global";
rekey_interval_location += " (";
rekey_interval_location += global_rekey_interval->getPosition().str();
rekey_interval_location += ")";
global_rekey_val = global_rekey_interval->intValue();
if ((global_rekey_val < 0) ||
(global_rekey_val > numeric_limits<uint32_t>::max())) {
isc_throw(BadValue, "'rekey-interval' parameter is out of "
"range [0.." << numeric_limits<uint32_t>::max()
<< "]" << rekey_interval_location);
}
}
string tkey_lifetime_origin = "default";
string tkey_lifetime_location = "";
int64_t global_tkey_lifetime_val = DnsServer::DEFAULT_KEY_LIFETIME;
ConstElementPtr global_tkey_lifetime = params->get("tkey-lifetime");
if (global_tkey_lifetime) {
tkey_lifetime_origin = "global";
tkey_lifetime_location += " (";
tkey_lifetime_location += global_tkey_lifetime->getPosition().str();
tkey_lifetime_location += ")";
global_tkey_lifetime_val = global_tkey_lifetime->intValue();
if ((global_tkey_lifetime_val < 0) ||
(global_tkey_lifetime_val > numeric_limits<uint32_t>::max())) {
isc_throw(BadValue, "'tkey-lifetime' parameter is out of "
"range [0.." << numeric_limits<uint32_t>::max()
<< "]" << tkey_lifetime_location);
}
}
if (global_retry_val >= global_rekey_val) {
isc_throw(BadValue, "the " << retry_interval_origin
<< " 'retry-interval' parameter"
<< retry_interval_location << " must be smaller then the "
<< rekey_interval_origin << " 'rekey-interval' parameter"
<< retry_interval_location << ": range [0.."
<< global_rekey_val << "]");
}
if (global_rekey_val >= global_tkey_lifetime_val) {
isc_throw(BadValue, "the " << rekey_interval_origin
<< " 'rekey-interval' parameter"
<< rekey_interval_location << " must be smaller than the "
<< tkey_lifetime_origin << " 'tkey-lifetime' parameter"
<< tkey_lifetime_location << ": range [0.."
<< global_tkey_lifetime_val << "]");
}
ConstElementPtr global_tkey_proto = params->get("tkey-protocol");
if (global_tkey_proto) {
string val = global_tkey_proto->stringValue();
if ((val != "UDP") && (val != "TCP")) {
isc_throw(BadValue, "'tkey-protocol' parameter must be UDP "
"or TCP (" << global_tkey_proto->getPosition() << ")");
}
}
ConstElementPtr global_fallback = params->get("fallback");
ConstElementPtr global_tkey_timeout = params->get("exchange-timeout");
if (global_tkey_timeout) {
int64_t val = global_tkey_timeout->intValue();
if ((val < 0) || (val > numeric_limits<uint32_t>::max())) {
isc_throw(BadValue, "'exchange-timeout' parameter is out of "
"range [0.." << numeric_limits<uint32_t>::max()
<< "] (" << global_tkey_timeout->getPosition() << ")");
}
}
ConstElementPtr servers = params->get("servers");
if (!servers) {
return;
}
uint32_t max_tkey_lifetime = 0;
for (auto const& map : servers->listValue()) {
if (!map) {
continue;
}
if (map->getType() != Element::map) {
isc_throw(BadValue, "'servers' parameter must be a list of "
"maps (" << map->getPosition() << ")");
}
try {
SimpleParser::checkKeywords(DnsServer::SERVER_PARAMETERS, map);
} catch (const DhcpConfigError& ex) {
isc_throw(BadValue, "gss_tsig server " << ex.what() << " ("
<< map->getPosition() << ")");
}
ConstElementPtr id_elem = map->get("id");
if (!id_elem) {
isc_throw(BadValue, "'id' parameter is required in "
"gss_tsig server entry (" << map->getPosition() << ")");
}
const string& id = id_elem->stringValue();
if (id.empty()) {
isc_throw(BadValue, "'id' parameter must be not empty in "
"gss_tsig server entry (" << map->getPosition() << ")");
}
if (getServer(id)) {
isc_throw(BadValue, "'" << id << "' id is already used in "
"gss_tsig server entry (" << map->getPosition() << ")");
}
ConstElementPtr domains_list = map->get("domain-names");
set<string> domains;
if (domains_list && !domains_list->empty()) {
for (auto const& domain : domains_list->listValue()) {
if (!domain) {
continue;
}
if (domain->getType() != Element::string) {
isc_throw(BadValue, "gss_tsig server 'domain-names' list "
<< "must contain only strings ("
<< domain->getPosition() << ")");
}
// Ignore duplicates.
static_cast<void>(domains.insert(domain->stringValue()));
}
}
DnsServerPtr srv;
ConstElementPtr ip_address = map->get("ip-address");
if (!ip_address) {
isc_throw(BadValue, "'ip-address' parameter is required in "
"gss_tsig server entry (" << map->getPosition() << ")");
}
try {
IOAddress addr(ip_address->stringValue());
if (map->contains("port")) {
int64_t port(SimpleParser::getInteger(map, "port", 0,
numeric_limits<uint16_t>::max()));
srv.reset(new DnsServer(id, domains, addr,
static_cast<uint16_t>(port)));
} else {
srv.reset(new DnsServer(id, domains, addr));
}
} catch (const DhcpConfigError& ex) {
isc_throw(BadValue, "gss_tsig bad server entry: " << ex.what());
} catch (const std::exception& ex) {
isc_throw(BadValue, "gss_tsig bad server entry: " << ex.what()
<< " (" << map->getPosition() << ")");
}
ConstElementPtr server_principal = map->get("server-principal");
bool server_principal_global = false;
if (!server_principal) {
server_principal = params->get("server-principal");
server_principal_global = true;
}
if (!server_principal) {
isc_throw(BadValue, "'server-principal' parameter is required in "
"gss_tsig server entry (" << map->getPosition() << ")");
}
srv->setServerPrincipal(server_principal->stringValue());
try {
srv->buildKeyNameSuffix();
} catch (const std::exception& ex) {
if (server_principal_global) {
isc_throw(BadValue, "gss_tsig bad server-principal parameter: "
<< ex.what() << " ("
<< server_principal->getPosition() << ")");
} else {
isc_throw(BadValue, "gss_tsig bad server entry: " << ex.what()
<< " (" << server_principal->getPosition() << ")");
}
}
ConstElementPtr gss_replay_flag = map->get("gss-replay-flag");
if (!gss_replay_flag) {
gss_replay_flag = params->get("gss-replay-flag");
}
if (gss_replay_flag) {
srv->setGssReplayFlag(gss_replay_flag->boolValue());
}
ConstElementPtr gss_sequence_flag = map->get("gss-sequence-flag");
if (!gss_sequence_flag) {
gss_sequence_flag = params->get("gss-sequence-flag");
}
if (gss_sequence_flag) {
srv->setGssSequenceFlag(gss_sequence_flag->boolValue());
}
ConstElementPtr cred_principal = map->get("client-principal");
if (!cred_principal) {
cred_principal = params->get("client-principal");
}
if (cred_principal) {
srv->setClientPrincipal(cred_principal->stringValue());
}
retry_interval_location = "";
ConstElementPtr retry_interval = map->get("retry-interval");
if (!retry_interval) {
retry_interval = global_retry_interval;
} else {
retry_interval_origin = "server";
}
int64_t retry_val = DnsServer::DEFAULT_RETRY_INTERVAL;
if (retry_interval) {
retry_interval_location += " (";
retry_interval_location += retry_interval->getPosition().str();
retry_interval_location += ")";
retry_val = retry_interval->intValue();
if ((retry_val < 0) ||
(retry_val > numeric_limits<uint32_t>::max())) {
isc_throw(BadValue, "'retry-interval' parameter is out of "
"range [0.." << numeric_limits<uint32_t>::max()
<< "]" << retry_interval_location);
}
srv->setRetryInterval(retry_val);
}
rekey_interval_location = "";
ConstElementPtr rekey_interval = map->get("rekey-interval");
if (!rekey_interval) {
rekey_interval = global_rekey_interval;
} else {
rekey_interval_origin = "server";
}
int64_t rekey_val = DnsServer::DEFAULT_REKEY_INTERVAL;
if (rekey_interval) {
rekey_interval_location += " (";
rekey_interval_location += rekey_interval->getPosition().str();
rekey_interval_location += ")";
rekey_val = rekey_interval->intValue();
if ((rekey_val < 0) ||
(rekey_val > numeric_limits<uint32_t>::max())) {
isc_throw(BadValue, "'rekey-interval' parameter is out of "
"range [0.." << numeric_limits<uint32_t>::max()
<< "]" << rekey_interval_location);
}
srv->setRekeyInterval(rekey_val);
}
tkey_lifetime_location = "";
ConstElementPtr tkey_lifetime = map->get("tkey-lifetime");
if (!tkey_lifetime) {
tkey_lifetime = global_tkey_lifetime;
} else {
tkey_lifetime_origin = "server";
}
int64_t tkey_lifetime_val = DnsServer::DEFAULT_KEY_LIFETIME;
if (tkey_lifetime) {
tkey_lifetime_location += " (";
tkey_lifetime_location += tkey_lifetime->getPosition().str();
tkey_lifetime_location += ")";
tkey_lifetime_val = tkey_lifetime->intValue();
if ((tkey_lifetime_val < 0) ||
(tkey_lifetime_val > numeric_limits<uint32_t>::max())) {
isc_throw(BadValue, "'tkey-lifetime' parameter is out of "
"range [0.." << numeric_limits<uint32_t>::max()
<< "]" << tkey_lifetime_location);
}
srv->setKeyLifetime(tkey_lifetime_val);
}
if (tkey_lifetime_val > max_tkey_lifetime) {
max_tkey_lifetime = tkey_lifetime_val;
}
if (retry_val >= rekey_val) {
isc_throw(BadValue, "the " << retry_interval_origin
<< " 'retry-interval' parameter"
<< retry_interval_location << " must be smaller then the "
<< rekey_interval_origin << " 'rekey-interval' parameter"
<< retry_interval_location << ": range [0.."
<< rekey_val << "]");
}
if (rekey_val >= tkey_lifetime_val) {
isc_throw(BadValue, "the " << rekey_interval_origin
<< " 'rekey-interval' parameter"
<< rekey_interval_location << " must be smaller than the "
<< tkey_lifetime_origin << " 'tkey-lifetime' parameter"
<< tkey_lifetime_location << ": range [0.."
<< tkey_lifetime_val << "]");
}
ConstElementPtr tkey_proto = map->get("tkey-protocol");
if (!tkey_proto) {
tkey_proto = global_tkey_proto;
}
if (tkey_proto) {
string val = tkey_proto->stringValue();
if (val == "UDP") {
srv->setKeyProto(IOFetch::UDP);
} else if (val == "TCP") {
srv->setKeyProto(IOFetch::TCP);
} else {
isc_throw(BadValue, "'tkey-protocol' parameter must be UDP "
"or TCP (" << tkey_proto->getPosition() << ")");
}
}
ConstElementPtr fallback = map->get("fallback");
if (!fallback) {
fallback = global_fallback;
}
if (fallback) {
srv->setFallback(fallback->boolValue());
}
ConstElementPtr tkey_timeout = params->get("exchange-timeout");
if (!tkey_timeout) {
tkey_timeout = global_tkey_timeout;
}
if (tkey_timeout) {
int64_t val = tkey_timeout->intValue();
if ((val < 0) || (val > numeric_limits<uint32_t>::max())) {
isc_throw(BadValue, "'exchange-timeout' parameter is out of "
"range [0.." << numeric_limits<uint32_t>::max()
<< "] (" << tkey_timeout->getPosition() << ")");
}
srv->setExchangeTimeout(val);
}
addServer(srv);
}
setMaxKeyLifetime(max_tkey_lifetime);
}
} // end of namespace isc::gss_tsig
} // end of namespace isc

View File

@ -0,0 +1,555 @@
// Copyright (C) 2021-2022 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 GSS_TSIG_CFG_H
#define GSS_TSIG_CFG_H
#include <asiodns/io_fetch.h>
#include <cc/cfg_to_element.h>
#include <cc/simple_parser.h>
#include <cc/user_context.h>
#include <d2srv/d2_cfg_mgr.h>
#include <boost/shared_ptr.hpp>
#include <boost/multi_index_container.hpp>
#include <boost/multi_index/hashed_index.hpp>
#include <boost/multi_index/mem_fun.hpp>
#include <boost/multi_index/random_access_index.hpp>
#include <list>
#include <map>
#include <memory>
#include <set>
#include <sstream>
#include <vector>
namespace isc {
namespace gss_tsig {
/// @brief GSS-TSIG hook configuration for a server.
class DnsServer : public isc::data::CfgToElement, public isc::data::UserContext {
public:
/// @brief This table defines all server parameters.
static const isc::data::SimpleKeywords SERVER_PARAMETERS;
/// @brief Server TKEY exchange statistics names.
///
/// - gss-tsig-key-created
/// - tkey-sent
/// - tkey-success
/// - tkey-timeout
/// - tkey-error
static const std::list<std::string> STAT_NAMES;
/// @brief Constructor.
///
/// A server is mainly recognized using its address and port.
///
/// @param id An identifier.
/// @param domains A list of DNS domains.
/// @param ip_address The server IP address.
/// @param port The server port (default 53).
DnsServer(const std::string& id,
const std::set<std::string>& domains,
const isc::asiolink::IOAddress& ip_address,
uint16_t port = isc::d2::DnsServerInfo::STANDARD_DNS_PORT);
/// @brief Destructor.
///
/// Remove the server statistics.
virtual ~DnsServer();
/// @brief Reset statistics.
virtual void resetStats();
/// @brief Get the ID.
///
/// @return the ID.
std::string getID() const {
return (id_);
}
/// @brief Set the ID.
///
/// @param id A new ID.
void setID(const std::string& id) {
id_ = id;
}
/// @brief Get the server IP address.
///
/// @return the server IP address.
const isc::asiolink::IOAddress& getIpAddress() const {
return (ip_address_);
}
/// @brief Get timer used to rekey or to retry on error.
///
/// @return The timer used to rekey or to retry on error.
isc::asiolink::IntervalTimerPtr& getTimer() {
return (timer_);
}
/// @brief Get the server port.
///
/// @return the server port.
uint16_t getPort() const {
return (port_);
}
/// @brief Get the server info list.
///
/// @return the server info list.
const isc::d2::DnsServerInfoStorage& getServerInfos() const {
return (server_infos_);
}
/// @brief Add a server info to the list.
///
/// @param server_info The new server info to add to the list.
void addServerInfo(isc::d2::DnsServerInfoPtr server_info) {
server_infos_.push_back(server_info);
}
/// @brief Clear the server info list.
void clearServerInfos() {
server_infos_.clear();
}
/// @brief Convert the list of DNS domains to the server info list.
///
/// For each domain in the list adds the corresponding server.
/// An empty list means to add all servers.
///
/// @param d2_config D2 configuration.
/// @throw isc::NotFound if a domain of the list is not served.
void buildServerInfo(isc::d2::D2CfgContextPtr d2_config);
/// @brief Get the DNS server principal.
///
/// @return the DNS server principal.
const std::string& getServerPrincipal() const {
return (server_principal_);
}
/// @brief Set the DNS server principal.
///
/// @param server_principal A new DNS server principal.
void setServerPrincipal(const std::string& server_principal) {
server_principal_ = server_principal;
}
/// @brief Get the GSS-TSIG key name suffix.
///
/// @return the GSS-TSIG key name suffix.
const std::string& getKeyNameSuffix() const {
return (key_name_suffix_);
}
/// @brief Set the GSS-TSIG key name suffix.
///
/// @param key_name_suffix A new GSS-TSIG key name suffix.
void setKeyNameSuffix(const std::string& key_name_suffix) {
key_name_suffix_ = key_name_suffix;
checkKeyNameSuffix();
}
/// @brief Build the GSS-TSIG key name suffix.
///
/// The GSS-TSIG key name suffix is the DNS part of the DNS server
/// principal i.e. the sub-string between '/' and '@'.
///
/// @throw BadValue when the DNS server principal has not the expected
/// format (this allows an error at argument parse time).
void buildKeyNameSuffix();
/// @brief Check and fix the GSS-TSIG key name suffix.
///
/// As a side effect a trailing dot can be added, e.g. when the
/// suffix was built from a Kerberos principal where usually
/// the domain part does not have one.
/// @note: if we want to canonize GSS-TSIG key name suffix this is
/// the right place to do this.
void checkKeyNameSuffix();
/// @brief Get the client/credentials principal.
///
/// @return the client/credentials principal.
const std::string& getClientPrincipal() const {
return (cred_principal_);
}
/// @brief Set the client/credentials principal.
///
/// @param cred_principal A new client/credentials principal.
void setClientPrincipal(const std::string& cred_principal) {
cred_principal_ = cred_principal;
}
/// @brief Get the TKEY lifetime.
///
/// @return the TKEY lifetime (expressed in seconds).
uint32_t getKeyLifetime() const {
return (tkey_lifetime_);
}
/// @brief Get the GSS (anti) replay flag.
///
/// @return the GSS (anti) replay flag.
bool getGssReplayFlag() const {
return (gss_replay_flag_);
}
/// @brief Set the GSS (anti) replay flag.
///
/// @param flag A new (anti) replay flag value.
void setGssReplayFlag(bool flag) {
gss_replay_flag_ = flag;
}
/// @brief Get the GSS sequence flag.
///
/// @return the GSS sequence flag.
bool getGssSequenceFlag() const {
return (gss_sequence_flag_);
}
/// @brief Set the GSS sequence flag.
///
/// @param flag A new sequence flag value.
void setGssSequenceFlag(bool flag) {
gss_sequence_flag_ = flag;
}
/// @brief Set the TKEY lifetime.
///
/// @param tkey_lifetime A new TKEY lifetime (expressed in seconds).
void setKeyLifetime(uint32_t tkey_lifetime) {
tkey_lifetime_ = tkey_lifetime;
}
/// @brief Get the rekey interval.
///
/// @return the rekey interval (expressed in seconds).
uint32_t getRekeyInterval() const {
return (rekey_interval_);
}
/// @brief Set the rekey interval.
///
/// @param rekey_interval A new rekey interval (expressed in seconds).
void setRekeyInterval(uint32_t rekey_interval) {
rekey_interval_ = rekey_interval;
}
/// @brief Get the retry interval.
///
/// @return the retry interval (expressed in seconds).
uint32_t getRetryInterval() const {
return (retry_interval_);
}
/// @brief Set the retry interval.
///
/// @param retry_interval A new retry interval (expressed in seconds).
void setRetryInterval(uint32_t retry_interval) {
retry_interval_ = retry_interval;
}
/// @brief Get the TKEY protocol.
///
/// @return the TKEY protocol.
isc::asiodns::IOFetch::Protocol getKeyProto() const {
return (tkey_proto_);
}
/// @brief Set the TKEY protocol.
///
/// @param tkey_proto A new TKEY protocol.
void setKeyProto(isc::asiodns::IOFetch::Protocol tkey_proto) {
tkey_proto_ = tkey_proto;
}
/// @brief Get the fallback flag.
///
/// @return the fallback flag.
bool getFallback() const {
return (fallback_);
}
/// @brief Set the fallback flag.
///
/// @param fallback A new fallback flag.
void setFallback(bool fallback) {
fallback_ = fallback;
}
/// @brief Get the TKEY exchange timeout.
///
/// @return the TKEY exchange timeout (expressed in milliseconds).
uint32_t getExchangeTimeout() const {
return (exchange_timeout_);
}
/// @brief Set the TKEY exchange timeout.
///
/// @param exchange_timeout A new TKEY exchange timeout (expressed in
/// milliseconds).
void setExchangeTimeout(uint32_t exchange_timeout) {
exchange_timeout_ = exchange_timeout;
}
/// @brief Unparse a DNS server object.
///
/// Used to get the full state of a DNS server.
/// @note: the GSS-TSIG key list is added by command handlers.
///
/// @return a pointer to unparsed DNS server object.
isc::data::ElementPtr toElement() const;
/// @brief The default TKEY lifetime (expressed in seconds).
///
/// Default value for TKEY lifetime: 3600 seconds (1 hour).
static constexpr size_t DEFAULT_KEY_LIFETIME = 3600;
/// @brief The rekey timer interval (expressed in seconds).
///
/// Default value for rekey timer: 2700 seconds (45 minutes).
static constexpr size_t DEFAULT_REKEY_INTERVAL = 2700;
/// @brief The retry timer interval (expressed in seconds).
///
/// Default value for retry timer: 120 seconds (2 minutes).
static constexpr size_t DEFAULT_RETRY_INTERVAL = 120;
/// @brief The default TKEY exchange timeout (expressed in milliseconds).
///
/// Default value for TKEY exchange timeout: 3000 milliseconds (3 seconds).
static constexpr size_t DEFAULT_EXCHANGE_TIMEOUT = 3000;
private:
/// @brief Initialize server statistics.
void initStats();
/// @brief Remove server statistics.
void removeStats();
/// @brief Convert a D2 DNS domain to a server info.
///
/// @param d2_dns_domain A D2 DNS domain.
/// @throw @c isc::NotFound if a domain of the list is not served.
void buildServerInfo(isc::d2::DdnsDomainPtr d2_dns_domain);
/// @brief The ID.
std::string id_;
/// @brief The list of domains.
std::set<std::string> domains_;
/// @brief The server IP address.
isc::asiolink::IOAddress ip_address_;
/// @brief The server port.
uint16_t port_;
/// @brief The server info list.
isc::d2::DnsServerInfoStorage server_infos_;
/// @brief The DNS server principal.
std::string server_principal_;
/// @brief The GSS-TSIG key name suffix.
std::string key_name_suffix_;
/// @brief The client/credentials principal.
std::string cred_principal_;
/// @brief The GSS (anti) replay flag.
bool gss_replay_flag_;
/// @brief The GSS sequence flag.
bool gss_sequence_flag_;
/// @brief The TKEY lifetime (expressed in seconds).
uint32_t tkey_lifetime_;
/// @brief The rekey interval (expressed in seconds).
uint32_t rekey_interval_;
/// @brief The retry interval (expressed in seconds).
uint32_t retry_interval_;
/// @brief The TKEY protocol.
isc::asiodns::IOFetch::Protocol tkey_proto_;
/// @brief The fallback flag (governs behavior when no key is available).
bool fallback_;
/// @brief The TKEY timeout (expressed in milliseconds).
uint32_t exchange_timeout_;
/// @brief The interval timer (used to rekey or to retry on error).
isc::asiolink::IntervalTimerPtr timer_;
};
/// @brief A pointer to a DNS server.
typedef boost::shared_ptr<DnsServer> DnsServerPtr;
/// @brief Tag for the random access index for searching DNS server.
struct DnsServerIndexTag { };
/// @brief Tag for the id access index for searching DNS server.
struct DnsServerIdTag { };
/// @brief A list of DNS server.
typedef boost::multi_index_container<
// Multi index container holding pointers to the DNS servers.
DnsServerPtr,
// The following holds all indexes.
boost::multi_index::indexed_by<
// First index allows the random access (vector).
boost::multi_index::random_access<
boost::multi_index::tag<DnsServerIndexTag>
>,
// Second index allows the id access.
boost::multi_index::hashed_unique<
boost::multi_index::tag<DnsServerIdTag>,
boost::multi_index::const_mem_fun<DnsServer, std::string,
&DnsServer::getID>
>
>
> DnsServerList;
/// @brief A map of DNS server info and DNS server.
typedef std::map<isc::d2::DnsServerInfoPtr, DnsServerPtr> DnsServerRevMap;
/// @brief GSS-TSIG hook configuration.
class GssTsigCfg {
public:
/// @brief This table defines all global parameters.
static const isc::data::SimpleKeywords GLOBAL_PARAMETERS;
/// @brief Constructor.
GssTsigCfg();
/// @brief Destructor.
virtual ~GssTsigCfg();
/// @brief Get the DNS server list.
///
/// @return the DNS server list.
const DnsServerList& getServerList() const {
return (servers_);
}
/// @brief Add a DNS server to the list.
///
/// @note: the caller must check if the server ID is already used.
///
/// @param server A new DNS server to add to the list.
void addServer(DnsServerPtr server) {
servers_.push_back(server);
}
/// @brief Get the DNS server reverse map.
///
/// @return the DNS server reverse map.
const DnsServerRevMap& getServerRevMap() const {
return (servers_rev_map_);
}
/// @brief Get the DNS server from a server info.
///
/// @param server_info The server info to find.
/// @return the DNS server or null if not found.
DnsServerPtr getServer(const isc::d2::DnsServerInfoPtr& server_info) const;
/// @brief Get the DNS server from its ID.
///
/// @param id The server ID.
/// @return the DNS server or null if not found.
DnsServerPtr getServer(const std::string& id) const;
/// @brief Clear the DNS server list and reverse map.
void clearServers() {
servers_.clear();
servers_rev_map_.clear();
}
/// @brief Build the reverse map.
///
/// @param d2_config D2 configuration.
void buildServerRevMap(isc::d2::D2CfgContextPtr d2_config);
/// @brief Get the client key table specification.
///
/// @return the client key table specification.
const std::string& getClientKeyTab() const {
return (client_keytab_);
}
/// @brief Set the client key table specification.
///
/// @param client_keytab A new client key table specification.
void setClientKeyTab(const std::string& client_keytab) {
client_keytab_ = client_keytab;
}
/// @brief Get the credentials cache specification.
///
/// @return the credentials cache specification.
const std::string& getCredsCache() const {
return (creds_cache_);
}
/// @brief Set the credentials cache specification.
///
/// @param creds_cache A new credentials cache specification.
void setCredsCache(const std::string& creds_cache) {
creds_cache_ = creds_cache;
}
/// @brief Configure.
///
/// @param params A map element with parameters.
/// @throw BadValue and similar exceptions on error.
void configure(isc::data::ConstElementPtr params);
/// @brief Get the maximum TKEY lifetime.
///
/// @return the maximum TKEY lifetime.
uint32_t getMaxKeyLifetime() const {
return (max_tkey_lifetime_);
}
/// @brief Set the maximum TKEY lifetime.
///
/// @param max_tkey_lifetime A new maximum TKEY lifetime.
void setMaxKeyLifetime(uint32_t max_tkey_lifetime) {
max_tkey_lifetime_ = max_tkey_lifetime;
}
private:
/// @brief The DNS server list.
DnsServerList servers_;
/// @brief The DNS server reverse map.
DnsServerRevMap servers_rev_map_;
/// @brief The client key table specification.
std::string client_keytab_;
/// @brief The credentials cache specification.
std::string creds_cache_;
/// @brief The maximum TKEY lifetime.
uint32_t max_tkey_lifetime_;
};
} // end of namespace isc::gss_tsig
} // end of namespace isc
#endif // GSS_TSIG_CFG_H

View File

@ -0,0 +1,467 @@
// Copyright (C) 2021-2024 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 <dns/rrclass.h>
#include <gss_tsig_context.h>
#include <gss_tsig_log.h>
#include <gss_tsig_messages.h>
#include <util/io.h>
#include <limits>
using namespace isc;
using namespace isc::d2;
using namespace isc::dns;
using namespace isc::dns::rdata::any;
using namespace isc::gss_tsig;
using namespace isc::log;
using namespace isc::util;
using namespace std;
namespace {
/// @brief DNS message header length.
const size_t MESSAGE_HEADER_LEN = 12;
/// @brief GSS-TSIG digest length (from bind9 dst_key_sigsize).
const size_t DIGEST_LEN = 128;
static_assert(DIGEST_LEN <= numeric_limits<uint16_t>::max(),
"DIGEST_LEN must fit in a uint16_t");
/// @brief Digest previous MAC.
///
/// @note: the previous MAC size always fits in 16 bits.
///
/// @param buffer Reference to the output buffer.
/// @param previous_digest Previous digest.
void
digestPreviousMAC(OutputBuffer& buffer, const vector<uint8_t>& previous_digest) {
if (previous_digest.empty()) {
// The previous digest was already used. We're in the middle of
// TCP stream somewhere and we already pushed some unsigned message
// into the MAC state.
return;
}
const uint16_t previous_digest_len(previous_digest.size());
buffer.writeUint16(previous_digest_len);
buffer.writeData(&previous_digest[0], previous_digest_len);
}
/// @brief Digest TSIG variables.
///
/// @param buffer Reference to the output buffer.
/// @param key The GSS-TSIG key.
/// @param rrclass Resource Record class.
/// @param ttl Resource Record Time To Live.
/// @param time_signed Time signed.
/// @param fudge Fudge.
/// @param error DNS error.
/// @param otherlen Other data length.
/// @param otherdata Pointer to other data.
/// @param time_variables_only True if only time variables must be digested.
void
digestTSIGVariables(OutputBuffer& buffer, const GssTsigKey& key,
uint16_t rrclass, uint32_t rrttl,
uint64_t time_signed, uint16_t fudge, uint16_t error,
uint16_t otherlen, const void* otherdata,
bool time_variables_only) {
if (!time_variables_only) {
key.getKeyName().toWire(buffer);
buffer.writeUint16(rrclass);
buffer.writeUint32(rrttl);
key.getAlgorithmName().toWire(buffer);
}
buffer.writeUint16(time_signed >> 32);
buffer.writeUint32(time_signed & 0xffffffff);
buffer.writeUint16(fudge);
if (!time_variables_only) {
buffer.writeUint16(error);
buffer.writeUint16(otherlen);
if (otherlen > 0) {
buffer.writeData(otherdata, otherlen);
}
}
}
/// @brief Digest DNS message.
///
/// Here, we exploit some minimum knowledge of DNS message format:
/// - the header section has a fixed length of 12 octets (MESSAGE_HEADER_LEN)
/// - the offset in the header section to the ID field is 0
/// - the offset in the header section to the ARCOUNT field is 10
/// (and the field length is 2 octets)
/// We could construct a separate Message object from the given data, adjust
/// fields via the Message interfaces and then render it back to a separate
/// buffer, but that would be overkilling. The DNS message header has a
/// fixed length and necessary modifications are quite straightforward, so
/// we do the job using lower level interfaces.
///
/// @param buffer Reference to the output buffer.
/// @param qid Original query ID.
/// @param data Points to the wire-format data (already checked for 0).
/// @param data_len The length of @c data in bytes (already checked to
/// be large enough).
void
digestDNSMessage(OutputBuffer& buffer, uint16_t qid, const void* data,
size_t data_len) {
if (!data || (data_len < MESSAGE_HEADER_LEN)) {
// Should not happen as it was already checked by the caller.
isc_throw(Unexpected, "digestDNSMessage called with bad data");
}
OutputBuffer hdr(MESSAGE_HEADER_LEN);
const uint8_t* msgptr = static_cast<const uint8_t*>(data);
// Install the original ID.
hdr.writeUint16(qid);
msgptr += sizeof(uint16_t);
// Copy the rest of the header except the ARCOUNT field.
hdr.writeData(msgptr, 8);
msgptr += 8;
// Install the adjusted ARCOUNT (we don't care even if the value is bogus
// and it underflows; it would simply result in verification failure).
hdr.writeUint16(InputBuffer(msgptr, sizeof(uint16_t)).readUint16() - 1);
msgptr += 2;
// Digest the header and the rest of the DNS message.
buffer.writeData(hdr.getData(), hdr.getLength());
buffer.writeData(msgptr, data_len - MESSAGE_HEADER_LEN);
}
}
namespace isc {
namespace gss_tsig {
GssTsigContext::GssTsigContext(GssTsigKey& key)
: TSIGContext(key), state_(INIT), key_(key), previous_digest_(),
error_(TSIGError::NOERROR()), previous_timesigned_(0),
last_sig_dist_(-1), tbs_(1024) {
}
GssTsigContext::~GssTsigContext() {
}
ConstTSIGRecordPtr
GssTsigContext::sign(const uint16_t qid, const void* const data,
const size_t data_len) {
if (state_ == VERIFIED_RESPONSE) {
isc_throw(TSIGContextError,
"TSIG sign attempt after verifying a response");
}
if (!data || (data_len == 0)) {
isc_throw(InvalidParameter, "TSIG sign error: empty data is given");
}
TSIGError error(TSIGError::NOERROR());
const uint64_t now = static_cast<uint64_t>(time(0));
// For responses adjust the error code.
if (state_ == RECEIVED_REQUEST) {
error = error_;
}
// For errors related to key or MAC, return an unsigned response as
// specified in Section 4.3 of RFC2845.
if (error == TSIGError::BAD_SIG() || error == TSIGError::BAD_KEY()) {
ConstTSIGRecordPtr
tsig(new TSIGRecord(key_.getKeyName(),
TSIG(key_.getAlgorithmName(),
now, DEFAULT_FUDGE, 0, 0,
qid, error.getCode(), 0, 0)));
previous_digest_.clear();
state_ = SENT_RESPONSE;
return (tsig);
}
if (!key_.getSecCtx().get()) {
isc_throw(Unexpected, "sign called with null security context");
}
// If the context has previous MAC (either the Request MAC or its own
// previous MAC), digest it.
if (state_ != INIT) {
digestPreviousMAC(tbs_, previous_digest_);
}
// Digest the message (without TSIG).
tbs_.writeData(data, data_len);
// Digest TSIG variables.
// First, prepare some non constant variables.
uint64_t time_signed = now;
uint16_t otherlen = 0;
OutputBuffer otherdata(otherlen);
// For BADTIME error, we include 6 bytes of other data.
// (6 bytes = size of time signed value).
if (error == TSIGError::BAD_TIME()) {
time_signed = previous_timesigned_;
otherlen = 6;
otherdata.writeUint16(now >> 32);
otherdata.writeUint32(now & 0xffffffff);
}
// Then calculate the digest. If state_ is SENT_RESPONSE we are sending
// a continued message in the same TCP stream so skip digesting
// variables except for time related variables (RFC2845 4.4).
digestTSIGVariables(tbs_, key_,
TSIGRecord::getClass().getCode(),
TSIGRecord::TSIG_TTL, time_signed,
DEFAULT_FUDGE, error.getCode(),
otherlen, otherlen ? otherdata.getData() : 0,
state_ == SENT_RESPONSE);
// Get the final digest, update internal state, then finish.
GssApiBuffer gtbs(tbs_.getLength(), tbs_.getData());
// Consume the to be signed buffer.
tbs_.clear();
GssApiBuffer gsign;
key_.getSecCtx().sign(gtbs, gsign);
if (gsign.getLength() > DIGEST_LEN) {
isc_throw(OutOfRange, "internal error: digest is larger than "
<< DIGEST_LEN);
}
ConstTSIGRecordPtr
tsig(new TSIGRecord(key_.getKeyName(),
TSIG(key_.getAlgorithmName(),
time_signed, DEFAULT_FUDGE,
gsign.getLength(), gsign.getValue(),
qid, error.getCode(), otherlen,
otherlen ? otherdata.getData() : 0)));
previous_digest_ = gsign.getContent();
if (state_ == INIT) {
state_ = SENT_REQUEST;
} else {
state_ = SENT_RESPONSE;
}
return (tsig);
}
TSIGError
GssTsigContext::verify(const dns::TSIGRecord* const record,
const void* const data, const size_t data_len) {
if (state_ == SENT_RESPONSE) {
isc_throw(TSIGContextError,
"TSIG verify attempt after sending a response");
}
// This helper method is used from verify(). It's expected to be called
// just before verify() returns. It updates internal state based on
// the verification result and return the TSIGError to be returned to
// the caller of verify(), so that verify() can call this method within
// its 'return' statement.
auto postVerifyUpdate = [&] (TSIGError error) -> TSIGError {
if (state_ == INIT) {
state_ = RECEIVED_REQUEST;
} else if ((state_ == SENT_REQUEST) &&
(error == TSIGError::NOERROR())) {
state_ = VERIFIED_RESPONSE;
}
error_ = error;
return (error);
};
// This code supports an obsolete feature of TSIG removed by RFC 8945:
// in a zone transfer it was allowed to not signed up to 99 consecutive
// messages. Note that all modern DNS servers sign all messages.
// Hopefully this code will be never used.
if (!record) {
if ((last_sig_dist_ >= 0) && (last_sig_dist_ < 99)) {
// It is not signed, but in the middle of TCP stream. We just
// update the MAC state and consider this message OK.
update(data, data_len);
// This one is not signed, the last signed is one message further
// now.
++last_sig_dist_;
// No digest to return now. Just say it's OK.
return (postVerifyUpdate(TSIGError::NOERROR()));
}
// This case happens when we sent a signed request and have received an
// unsigned response. According to RFC2845 Section 4.6 this case should be
// considered a "format error" (although the specific error code
// wouldn't matter much for the caller).
return (postVerifyUpdate(TSIGError::FORMERR()));
}
const TSIG& tsig_rdata = record->getRdata();
if (data_len < MESSAGE_HEADER_LEN + record->getLength()) {
isc_throw(InvalidParameter,
"TSIG verify: data length is invalid: " << data_len);
}
if (!data) {
isc_throw(InvalidParameter, "TSIG verify: empty data is invalid");
}
if (!key_.getSecCtx().get()) {
isc_throw(Unexpected, "verify called with null security context");
}
// This message is signed and we won't throw any more.
last_sig_dist_ = 0;
// Check key: whether we first verify it with a known key or we verify
// it using the consistent key in the context. If the check fails we are
// done with BADKEY.
if ((state_ == INIT) && (error_ == TSIGError::BAD_KEY())) {
return (postVerifyUpdate(TSIGError::BAD_KEY()));
}
if ((key_.getKeyName() != record->getName()) ||
(key_.getAlgorithmName() != tsig_rdata.getAlgorithm())) {
return (postVerifyUpdate(TSIGError::BAD_KEY()));
}
// Check time: the current time must be in the range of
// [time signed - fudge, time signed + fudge]. Otherwise verification
// fails with BADTIME. (RFC2845 Section 4.6.2)
// Note: for simplicity we don't explicitly catch the case of too small
// current time causing underflow. With the fact that fudge is quite
// small and (for now) non configurable, it shouldn't be a real concern
// in practice.
const uint64_t now = static_cast<uint64_t>(time(0));
if (tsig_rdata.getTimeSigned() + DEFAULT_FUDGE < now ||
tsig_rdata.getTimeSigned() - DEFAULT_FUDGE > now) {
if (state_ == INIT) {
const uint8_t* digest =
static_cast<const uint8_t*>(tsig_rdata.getMAC());
if (digest) {
previous_digest_.assign(digest,
digest + tsig_rdata.getMACSize());
}
previous_timesigned_ = tsig_rdata.getTimeSigned();
}
return (postVerifyUpdate(TSIGError::BAD_TIME()));
}
// Handling empty MAC. While RFC2845 doesn't explicitly prohibit other
// cases, it can only reasonably happen in a response with BADSIG or
// BADKEY. We reject other cases as if it were BADSIG to avoid unexpected
// acceptance of a bogus signature. This behavior follows the BIND 9
// implementation.
if (tsig_rdata.getMACSize() == 0) {
TSIGError error = TSIGError(tsig_rdata.getError());
if ((error != TSIGError::BAD_SIG()) &&
(error != TSIGError::BAD_KEY())) {
error = TSIGError::BAD_SIG();
}
return (postVerifyUpdate(error));
}
// If the context has previous MAC (either the Request MAC or its own
// previous MAC), digest it.
if (state_ != INIT) {
digestPreviousMAC(tbs_, previous_digest_);
}
// No signature length check with GSS-TSIG.
//
// Digest DNS message (excluding the trailing TSIG RR and adjusting the
// QID and ARCOUNT header fields)
//
digestDNSMessage(tbs_, tsig_rdata.getOriginalID(),
data, data_len - record->getLength());
// Digest TSIG variables. If state_ is VERIFIED_RESPONSE, it's a
// continuation of the same TCP stream and skip digesting them except
// for time related variables (RFC2845 4.4).
// Note: we use the constant values for RR class and TTL specified
// in RFC2845, not received values (we reject other values in constructing
// the TSIGRecord).
digestTSIGVariables(tbs_, key_,
TSIGRecord::getClass().getCode(),
TSIGRecord::TSIG_TTL,
tsig_rdata.getTimeSigned(),
tsig_rdata.getFudge(), tsig_rdata.getError(),
tsig_rdata.getOtherLen(),
tsig_rdata.getOtherData(),
state_ == VERIFIED_RESPONSE);
// Verify the digest with the received signature.
try {
GssApiBuffer gtbv(tbs_.getLength(), tbs_.getData());
// Consume the to be verified buffer.
tbs_.clear();
GssApiBuffer gsign(tsig_rdata.getMACSize(), tsig_rdata.getMAC());
key_.getSecCtx().verify(gtbv, gsign);
LOG_DEBUG(gss_tsig_logger, DBGLVL_TRACE_BASIC, GSS_TSIG_VERIFIED);
const uint8_t* digest =
static_cast<const uint8_t*>(tsig_rdata.getMAC());
previous_digest_.assign(digest, digest + tsig_rdata.getMACSize());
return (postVerifyUpdate(TSIGError::NOERROR()));
} catch (const Exception& ex) {
LOG_INFO(gss_tsig_logger, GSS_TSIG_VERIFY_FAILED)
.arg(ex.what());
}
return (postVerifyUpdate(TSIGError::BAD_SIG()));
}
bool
GssTsigContext::lastHadSignature() const {
if (last_sig_dist_ == -1) {
isc_throw(TSIGContextError, "No message was verified yet");
}
return (last_sig_dist_ == 0);
}
size_t
GssTsigContext::getTSIGLength() const {
//
// The space required for an TSIG record is:
//
// n1 bytes for the (key) name
// 2 bytes for the type
// 2 bytes for the class
// 4 bytes for the ttl
// 2 bytes for the rdlength
// n2 bytes for the algorithm name
// 6 bytes for the time signed
// 2 bytes for the fudge
// 2 bytes for the MAC size
// x bytes for the MAC
// 2 bytes for the original id
// 2 bytes for the error
// 2 bytes for the other data length
// y bytes for the other data (at most)
// ---------------------------------
// 26 + n1 + n2 + x + y bytes
//
// Normally the digest length ("x") is the length of the underlying
// hash output. If a key related error occurred, however, the
// corresponding TSIG will be "unsigned", and the digest length will be 0.
size_t digest_len = DIGEST_LEN;
if (error_ == TSIGError::BAD_KEY() || error_ == TSIGError::BAD_SIG()) {
digest_len = 0;
}
// Other Len ("y") is normally 0; if BAD_TIME error occurred, the
// subsequent TSIG will contain 48 bits of the server current time.
size_t other_len = 0;
if (error_ == TSIGError::BAD_TIME()) {
other_len = 6;
}
return (26 + key_.getKeyName().getLength() +
key_.getAlgorithmName().getLength() +
digest_len + other_len);
}
void
GssTsigContext::update(const void* const data, size_t len) {
// Use the previous digest and never use it again.
digestPreviousMAC(tbs_, previous_digest_);
previous_digest_.clear();
// Push the message there.
tbs_.writeData(data, len);
}
} // end of namespace isc::gss_tsig
} // end of namespace isc

View File

@ -0,0 +1,183 @@
// Copyright (C) 2021-2022 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 gss_tsig_context.h Implements a TSIGContext derived class which
/// can be used as the value of TSIGContext pointers so with minimal or
/// update to the DNS++ library. The constructor takes a GSS-TSIG key
/// to share the associated GSS-API security context.
#ifndef GSS_TSIG_CONTEXT_H
#define GSS_TSIG_CONTEXT_H
#include <dns/tsig.h>
#include <gss_tsig_key.h>
#include <boost/shared_ptr.hpp>
namespace isc {
namespace gss_tsig {
/// @brief GSS-TSIG overwrite of the DNS TSIGContext class.
///
/// src/lib/dns/tsig.h includes a description of TSIGContext API.
///
/// The last TKEY response is signed but this is outside the state
/// transitions which are:
///
/// client side:
/// - INIT
/// - send a signed request
/// - SENT_REQUEST
/// - receive a signed response
/// - VERIFIED_RESPONSE
///
/// server side:
/// - INIT
/// - receive a signed request
/// - RECEIVED_REQUEST
/// - send a signed response
///
/// For TKEY on the reception of the last signed response:
/// - use the payload to finish the GSS-API security context establishment
/// - check the presence of a TSIG
/// - create a GssTsigContext object
/// - set its state to SENT_REQUEST
/// - verify the response using a pointer to the GssTsigContext object
///
/// For each exchange a different TSIGContext is required (no clear operation).
class GssTsigContext : public dns::TSIGContext {
public:
/// @brief Constructor.
///
/// @param key GSS-TSIG key.
explicit GssTsigContext(GssTsigKey& key);
/// @brief Destructor.
virtual ~GssTsigContext();
/// @brief Sign a DNS message.
///
/// See @c isc::dns::TSIGContext::sign().
///
/// @param qid The QID to be as the value of the original ID field of
/// the resulting TSIG record.
/// @param data The wire-format data to be signed.
/// @param data_len The length of @c data in bytes.
/// @return A TSIG record for the given data along with the context.
virtual dns::ConstTSIGRecordPtr
sign(const uint16_t qid, const void* const data,
const size_t data_len) override;
/// @brief a DNS message.
///
/// See @c isc::dns::TSIGContext::verify().
///
virtual dns::TSIGError
verify(const dns::TSIGRecord* const record, const void* const data,
const size_t data_len) override;
/// @brief Check whether the last verified message was signed.
///
/// See @c isc::dns::TSIGContext::lastHadSignature().
///
/// @return If the last message was signed or not.
/// @exception TSIGContextError if no message was verified yet.
virtual bool lastHadSignature() const override;
/// @brief Return the expected length of TSIG RR after @c sign().
///
/// See @c isc::dns::TSIGContext::getTSIGLength().
///
/// @note: use the fixed constant of 128 from bind9.
///
/// @return The expected TSIG RR length in bytes.
virtual size_t getTSIGLength() const override;
/// @brief Return the current state of the context.
///
/// See @c isc::dns::TSIGContext::getState().
///
/// @exception None.
virtual State getState() const override {
return (state_);
}
/// @brief Set the current state of the context.
///
/// @note: to be used for the last TKEY response.
///
/// @param state New state.
virtual void setState(State state) {
state_ = state;
}
/// @brief Return the TSIG error as a result of the latest verification.
///
/// See @c isc::dns::TSIGContext::getError().
///
/// @exception None.
virtual dns::TSIGError getError() const override {
return (error_);
}
/// @brief Set the TSIG error.
///
/// @note: to be used for the last TKEY response.
///
/// @param error New error.
virtual void setError(dns::TSIGError error) {
error_ = error;
}
private:
/// @brief State.
State state_;
/// @brief GSS-TSIG key.
GssTsigKey& key_;
/// @brief Previous digest.
std::vector<uint8_t> previous_digest_;
/// @brief TSIG error.
dns::TSIGError error_;
/// @brief Previous time signed.
///
/// @note: only meaningful for response with BADTIME.
uint64_t previous_timesigned_;
/// @brief Distance from the last verified signed message.
///
/// @note: Value of 0 means the last message was signed.
/// Special value -1 means there was no signed message yet.
int last_sig_dist_;
/// @brief To be signed (or verified) buffer.
util::OutputBuffer tbs_;
protected:
/// @brief Update internal MAC state by more data.
/// This is used mostly internally, when we need to verify a message without
/// TSIG signature in the middle of signed TCP stream. However, it is also
/// used in tests, so it's protected instead of private, to allow tests
/// in.
///
/// It doesn't contain sanity checks, and it is not tested directly. But
/// we may want to add these one day to allow generating the skipped TSIG
/// messages too. Until then, do not use this method.
///
/// @param data Points to the wire-format data.
/// @param len The length of @c data in bytes.
void update(const void* const data, size_t len);
};
/// @brief Type of pointer to a GSS-TSIG context.
typedef boost::shared_ptr<GssTsigContext> GssTsigContextPtr;
} // end of namespace isc::gss_tsig
} // end of namespace isc
#endif // GSS_TSIG_CONTEXT_H

View File

@ -0,0 +1,957 @@
// Copyright (C) 2021-2024 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 <cc/command_interpreter.h>
#include <stats/stats_mgr.h>
#include <gss_tsig_impl.h>
#include <gss_tsig_log.h>
#include <boost/foreach.hpp>
#include <cstdlib>
#include <list>
#include <sstream>
using namespace isc;
using namespace isc::asiolink;
using namespace isc::config;
using namespace isc::d2;
using namespace isc::data;
using namespace isc::hooks;
using namespace isc::log;
using namespace isc::stats;
using namespace std;
using namespace std::chrono;
namespace isc {
namespace gss_tsig {
GssTsigImpl::GssTsigImpl() : cfg_(), keys_(), io_service_(new IOService()),
krb5_client_ktname_prev_(), krb5ccname_prev_(), purge_timer_() {
}
GssTsigImpl::~GssTsigImpl() {
stop();
io_service_->stopAndPoll();
}
void
GssTsigImpl::configure(ConstElementPtr config) {
cfg_.configure(config);
if (!cfg_.getClientKeyTab().empty()) {
char* krb5_client_ktname = getenv("KRB5_CLIENT_KTNAME");
if (krb5_client_ktname) {
krb5_client_ktname_prev_.reset(new string(krb5_client_ktname));
} else {
krb5_client_ktname_prev_.reset();
}
setenv("KRB5_CLIENT_KTNAME", cfg_.getClientKeyTab().c_str(), 1);
}
if (!cfg_.getCredsCache().empty()) {
char* krb5ccname = getenv("KRB5CCNAME");
if (krb5ccname) {
krb5ccname_prev_.reset(new string(krb5ccname));
} else {
krb5ccname_prev_.reset();
}
setenv("KRB5CCNAME", cfg_.getCredsCache().c_str(), 1);
}
StatsMgr& stats_mgr = StatsMgr::instance();
for (auto const& name : DnsServer::STAT_NAMES) {
stats_mgr.setValue(name, static_cast<int64_t>(0));
}
}
void
GssTsigImpl::finishConfigure(D2CfgContextPtr d2_config) {
cfg_.buildServerRevMap(d2_config);
}
void
GssTsigImpl::start() {
LOG_DEBUG(gss_tsig_logger, DBGLVL_TRACE_BASIC, GSS_TSIG_MANAGER_STARTED);
for (auto const& server : cfg_.getServerList()) {
if (!server) {
continue;
}
server->getTimer().reset(new IntervalTimer(io_service_));
}
processAllServersKeys();
uint32_t max_tkey_lifetime = cfg_.getMaxKeyLifetime();
if (max_tkey_lifetime > 0) {
purge_timer_.reset(new IntervalTimer(io_service_));
purge_timer_->setup([this]() { purgeKeys(); },
max_tkey_lifetime * 1000,
IntervalTimer::REPEATING);
}
}
void
GssTsigImpl::stop() {
LOG_DEBUG(gss_tsig_logger, DBGLVL_TRACE_BASIC, GSS_TSIG_MANAGER_STOPPED);
if (purge_timer_) {
purge_timer_->cancel();
purge_timer_.reset();
}
for (auto const& server : cfg_.getServerList()) {
if (!server) {
continue;
}
if (server->getTimer()) {
server->getTimer()->cancel();
server->getTimer().reset();
}
}
for (auto const& key : keys_) {
key->getTKeyExchange().reset();
}
keys_.clear();
cfg_.clearServers();
// Run cancelled callbacks.
if (io_service_) {
try {
io_service_->poll();
} catch (const std::exception& ex) {
LOG_ERROR(gss_tsig_logger, GSS_TSIG_MANAGER_STOP_ERROR)
.arg(ex.what());
} catch (...) {
LOG_ERROR(gss_tsig_logger, GSS_TSIG_MANAGER_STOP_GENERAL_ERROR);
}
}
// Remove statistics.
StatsMgr& stats_mgr = StatsMgr::instance();
for (auto const& name : DnsServer::STAT_NAMES) {
stats_mgr.del(name);
}
// Restore environment variables.
if (!cfg_.getClientKeyTab().empty()) {
if (krb5_client_ktname_prev_) {
setenv("KRB5_CLIENT_KTNAME", krb5_client_ktname_prev_->c_str(), 1);
} else {
unsetenv("KRB5_CLIENT_KTNAME");
}
}
if (!cfg_.getCredsCache().empty()) {
if (krb5ccname_prev_) {
setenv("KRB5CCNAME", krb5ccname_prev_->c_str(), 1);
} else {
unsetenv("KRB5CCNAME");
}
}
}
void
GssTsigImpl::createKey(DnsServerPtr server,
time_point<std::chrono::system_clock> now) {
StatsMgr& stats_mgr = StatsMgr::instance();
stats_mgr.addValue("gss-tsig-key-created",
static_cast<int64_t>(1));
stats_mgr.addValue(StatsMgr::generateName("server",
server->getID(),
"gss-tsig-key-created"),
static_cast<int64_t>(1));
string name;
for (;;) {
name = ManagedKey::genName(server->getKeyNameSuffix());
if (keys_.count(name) == 0) {
break;
}
// Very unlikely but possible name collision.
// Try another name.
}
LOG_INFO(gss_tsig_logger, GSS_TSIG_NEW_KEY).arg(name);
ManagedKeyPtr mkey(new ManagedKey(name));
mkey->setParentID(server->getID());
mkey->setInception(now);
mkey->setExpire(now + seconds(server->getKeyLifetime()));
static_cast<void>(keys_.insert(mkey));
OM_uint32 flags = TKeyExchange::TKEY_EXCHANGE_FLAGS;
if (!server->getGssReplayFlag()) {
flags &= ~GSS_C_REPLAY_FLAG;
}
if (server->getGssSequenceFlag()) {
flags |= GSS_C_SEQUENCE_FLAG;
}
mkey->getTKeyExchange().reset(new TKeyExchange(io_service_, server, mkey,
mkey.get(),
server->getExchangeTimeout(),
flags));
io_service_->post([mkey]() {
if (mkey->getTKeyExchange()) {
mkey->getTKeyExchange()->doExchange();
}
});
}
void
GssTsigImpl::processAllServersKeys(bool rekey) {
for (auto const& server : cfg_.getServerList()) {
processServerKeys(server, rekey);
}
}
void
GssTsigImpl::processServerKeys(DnsServerPtr server, bool rekey) {
if (!server) {
return;
}
bool retry = false;
ManagedKeyPtr newest;
auto const now = system_clock::now();
const std::chrono::seconds retry_dur(server->getRetryInterval());
const std::chrono::seconds rekey_dur(server->getRekeyInterval());
auto const& idx = keys_.get<GssTsigKeyServerTag>();
auto const& range = idx.equal_range(server->getID());
try {
BOOST_FOREACH(auto const& key, range) {
lock_guard<mutex> lock(*key->mutex_);
if (!newest || (newest->getInception() < key->getInception())) {
newest = key;
}
if (key->getStatus() == ManagedKey::USABLE) {
if (now >= key->getExpire()) {
key->setStatus(ManagedKey::EXPIRED);
}
}
}
// Rekey if no key or the newest key is not usable but ready.
if (!newest) {
rekey = true;
} else {
switch (newest->getStatus()) {
case ManagedKey::NOT_READY:
// Lost some race, check again later.
retry = true;
break;
case ManagedKey::USABLE:
break;
default:
rekey = true;
break;
}
}
// Rekey too if the newest key is usable but soon to expire.
if (!rekey &&
((now + retry_dur >= newest->getExpire()) ||
(now >= newest->getInception() + rekey_dur))) {
rekey = true;
}
// Create and setup a new GSS-TSIG key.
if (rekey) {
createKey(server, now);
retry = true;
}
} catch (const std::exception& ex) {
retry = true;
LOG_ERROR(gss_tsig_logger, KEY_PROCESSING_FAILED)
.arg(server->getID())
.arg(ex.what());
} catch (...) {
retry = true;
LOG_ERROR(gss_tsig_logger, KEY_PROCESSING_FAILED_UNSPECIFIED_ERROR)
.arg(server->getID());
}
auto timer = server->getTimer();
if (!timer) {
return;
}
timer->cancel();
// Schedule a retry.
if (retry || !newest) {
timer->setup([this, server]() { processServerKeys(server); },
server->getRetryInterval() * 1000,
IntervalTimer::ONE_SHOT);
LOG_DEBUG(gss_tsig_logger, DBGLVL_TRACE_BASIC, START_RETRY_TIMER)
.arg(server->getID())
.arg(server->getRetryInterval());
return;
}
// Schedule a rekey.
auto const rekey_date = newest->getInception() + rekey_dur;
const std::chrono::system_clock::duration rekey_interval(rekey_date - now);
auto interval =
(duration_cast<nanoseconds>(rekey_interval).count() + 999999) / 1000000;
if (interval < server->getRetryInterval()) {
interval = server->getRetryInterval();
}
timer->setup([this, server]() { processServerKeys(server); },
interval, IntervalTimer::ONE_SHOT);
LOG_DEBUG(gss_tsig_logger, DBGLVL_TRACE_BASIC, START_REKEY_TIMER)
.arg(server->getID())
.arg(interval / 1000);
}
DnsServerPtr
GssTsigImpl::getServer(const std::string& id) const {
return (cfg_.getServer(id));
}
ManagedKeyPtr
GssTsigImpl::findKey(const d2::DnsServerInfoPtr& server_info,
bool& useGssTsig, bool& fallback) {
DnsServerPtr server = cfg_.getServer(server_info);
if (!server) {
LOG_DEBUG(gss_tsig_logger, DBGLVL_TRACE_BASIC, KEY_LOOKUP_DISABLED);
useGssTsig = false;
fallback = false;
return (ManagedKeyPtr());
}
useGssTsig = true;
fallback = server->getFallback();
auto now = system_clock::now();
auto const& idx = keys_.get<GssTsigKeyServerTag>();
auto const& range = idx.equal_range(server->getID());
ManagedKeyPtr candidate;
BOOST_FOREACH(auto const& key, range) {
lock_guard<mutex> lock(*key->mutex_);
if (key->getStatus() == ManagedKey::USABLE) {
if (now >= key->getExpire()) {
key->setStatus(ManagedKey::EXPIRED);
continue;
}
candidate = key;
}
}
if (candidate) {
LOG_DEBUG(gss_tsig_logger, DBGLVL_TRACE_BASIC, KEY_LOOKUP_FOUND)
.arg(candidate->getKeyNameStr());
return (candidate);
}
LOG_DEBUG(gss_tsig_logger, DBGLVL_TRACE_BASIC, KEY_LOOKUP_NONE);
return (ManagedKeyPtr());
}
ManagedKeyPtr
GssTsigImpl::findKey(const std::string& name) const {
auto const& it = keys_.find(name);
if (it == keys_.cend()) {
return (ManagedKeyPtr());
}
return (*it);
}
void
GssTsigImpl::purgeKeys() {
// Build the list of keys to remove.
auto now = system_clock::now();
std::chrono::seconds max_age(cfg_.getMaxKeyLifetime() * 3);
list<ManagedKeyPtr> to_purge;
for (auto const& key : keys_) {
lock_guard<mutex> lock(*key->mutex_);
if (key->getExpire() + max_age < now) {
to_purge.push_back(key);
}
}
// Return now if there is no key to remove.
if (to_purge.empty()) {
return;
}
// Remove all elements of the list.
for (auto const& key : to_purge) {
auto it = keys_.find(key->getKeyNameStr());
if (it != keys_.end()) {
key->getTKeyExchange().reset();
keys_.erase(it);
}
}
/// log a message with the count.
LOG_DEBUG(gss_tsig_logger, DBGLVL_TRACE_BASIC, GSS_TSIG_OLD_KEY_REMOVED)
.arg(to_purge.size());
}
void
GssTsigImpl::getHandler(CalloutHandle& handle) const {
ConstElementPtr response;
string id;
DnsServerPtr server;
try {
// Command is always provided.
ConstElementPtr command;
handle.getArgument("command", command);
// Retrieve arguments.
ConstElementPtr args;
static_cast<void>(parseCommand(args, command));
// Arguments are required.
if (!args) {
isc_throw(BadValue, "arguments not found in the '"
<< command->str() << "' command");
}
// Arguments must be a map.
if (args->getType() != Element::map) {
isc_throw(BadValue, "arguments in the '" << command->str()
<< "' command are not a map");
}
// server-id is mandatory.
ConstElementPtr server_id = args->get("server-id");
if (!server_id) {
isc_throw(BadValue, "'server-id' is mandatory for the '"
<< command->str() << "' command");
}
// server-id must be a string.
if (server_id->getType() != Element::string) {
isc_throw(BadValue, "'server-id' must be a string in the '"
<< command->str() << "' command");
}
id = server_id->stringValue();
server = cfg_.getServer(id);
} catch (const std::exception& ex) {
// There was an error while parsing command arguments.
// Return an error status code to notify the user.
response = createAnswer(CONTROL_RESULT_ERROR, ex.what());
handle.setArgument("response", response);
return;
}
ostringstream msg;
msg << "GSS-TSIG server[" << id << "] ";
// No server.
if (!server) {
msg << "not found";
response = createAnswer(CONTROL_RESULT_EMPTY, msg.str());
} else {
msg << "found";
ElementPtr desc = server->toElement();
ElementPtr keys = Element::createList();
auto const& idx = keys_.get<GssTsigKeyServerTag>();
auto const& range = idx.equal_range(server->getID());
BOOST_FOREACH(auto const& key, range) {
keys->add(key->toElement());
}
desc->set("keys", keys);
response = createAnswer(CONTROL_RESULT_SUCCESS, msg.str(), desc);
}
handle.setArgument("response", response);
}
void
GssTsigImpl::getAllHandler(CalloutHandle& handle) const {
ConstElementPtr response;
// Create a list where we're going to store servers' information.
ElementPtr servers = Element::createList();
// Create arguments map and add server list.
ElementPtr args = Element::createMap();
args->set("gss-tsig-servers", servers);
auto const& idx = keys_.get<GssTsigKeyServerTag>();
size_t key_count(0);
// Iterate over servers.
for (auto const& server : cfg_.getServerList()) {
ElementPtr desc = server->toElement();
ElementPtr keys = Element::createList();
auto const& range = idx.equal_range(server->getID());
BOOST_FOREACH(auto const& key, range) {
keys->add(key->toElement());
}
desc->set("keys", keys);
key_count += keys->size();
servers->add(desc);
}
ostringstream msg;
msg << servers->size() << " GSS-TSIG servers";
if (servers->empty()) {
response = createAnswer(CONTROL_RESULT_EMPTY, msg.str(), args);
} else {
msg << " and " << key_count << " keys";
response = createAnswer(CONTROL_RESULT_SUCCESS, msg.str(), args);
}
handle.setArgument("response", response);
}
void
GssTsigImpl::listHandler(CalloutHandle& handle) const {
ConstElementPtr response;
// Create a list where we're going to store servers' ID.
ElementPtr servers = Element::createList();
// Create a list where we're going to store keys' name.
ElementPtr keys = Element::createList();
// Create arguments map and add server and key lists.
ElementPtr args = Element::createMap();
args->set("gss-tsig-servers", servers);
args->set("gss-tsig-keys", keys);
// Iterate over servers.
for (auto const& server : cfg_.getServerList()) {
servers->add(Element::create(server->getID()));
}
// Iterate over keys.
for (auto const& key : keys_) {
keys->add(Element::create(key->getKeyNameStr()));
}
ostringstream msg;
msg << servers->size() << " GSS-TSIG servers and "
<< keys->size() << " keys";
if (servers->empty() && keys->empty()) {
response = createAnswer(CONTROL_RESULT_EMPTY, msg.str(), args);
} else {
response = createAnswer(CONTROL_RESULT_SUCCESS, msg.str(), args);
}
handle.setArgument("response", response);
}
void
GssTsigImpl::keyGetHandler(CalloutHandle& handle) const {
ConstElementPtr response;
string name;
ManagedKeyPtr key;
try {
// Command is always provided.
ConstElementPtr command;
handle.getArgument("command", command);
// Retrieve arguments.
ConstElementPtr args;
static_cast<void>(parseCommand(args, command));
// Arguments are required.
if (!args) {
isc_throw(BadValue, "arguments not found in the '"
<< command->str() << "' command");
}
// Arguments must be a map.
if (args->getType() != Element::map) {
isc_throw(BadValue, "arguments in the '" << command->str()
<< "' command are not a map");
}
// key-name is mandatory.
ConstElementPtr key_name = args->get("key-name");
if (!key_name) {
isc_throw(BadValue, "'key-name' is mandatory for the '"
<< command->str() << "' command");
}
// key-name must be a string.
if (key_name->getType() != Element::string) {
isc_throw(BadValue, "'key-name' must be a string in the '"
<< command->str() << "' command");
}
name = key_name->stringValue();
key = findKey(name);
} catch (const std::exception& ex) {
// There was an error while parsing command arguments.
// Return an error status code to notify the user.
response = createAnswer(CONTROL_RESULT_ERROR, ex.what());
handle.setArgument("response", response);
return;
}
ostringstream msg;
msg << "GSS-TSIG key '" << name << "' ";
// No key.
if (!key) {
msg << "not found";
response = createAnswer(CONTROL_RESULT_EMPTY, msg.str());
} else {
msg << "found";
ConstElementPtr desc = key->toElement();
response = createAnswer(CONTROL_RESULT_SUCCESS, msg.str(), desc);
}
handle.setArgument("response", response);
}
void
GssTsigImpl::keyExpireHandler(CalloutHandle& handle) {
ConstElementPtr response;
string name;
ManagedKeyPtr key;
try {
// Command is always provided.
ConstElementPtr command;
handle.getArgument("command", command);
// Retrieve arguments.
ConstElementPtr args;
static_cast<void>(parseCommand(args, command));
// Arguments are required.
if (!args) {
isc_throw(BadValue, "arguments not found in the '"
<< command->str() << "' command");
}
// Arguments must be a map.
if (args->getType() != Element::map) {
isc_throw(BadValue, "arguments in the '" << command->str()
<< "' command are not a map");
}
// key-name is mandatory.
ConstElementPtr key_name = args->get("key-name");
if (!key_name) {
isc_throw(BadValue, "'key-name' is mandatory for the '"
<< command->str() << "' command");
}
// key-name must be a string.
if (key_name->getType() != Element::string) {
isc_throw(BadValue, "'key-name' must be a string in the '"
<< command->str() << "' command");
}
name = key_name->stringValue();
key = findKey(name);
} catch (const std::exception& ex) {
// There was an error while parsing command arguments.
// Return an error status code to notify the user.
response = createAnswer(CONTROL_RESULT_ERROR, ex.what());
handle.setArgument("response", response);
return;
}
ostringstream msg;
msg << "GSS-TSIG key '" << name << "' ";
// No key.
if (!key) {
msg << "not found";
response = createAnswer(CONTROL_RESULT_EMPTY, msg.str());
} else {
bool can_expire = true;
{
lock_guard<mutex> lock(*key->mutex_);
if (key->getStatus() >= ManagedKey::EXPIRED) {
can_expire = false;
} else {
key->setStatus(ManagedKey::EXPIRED);
}
}
if (!can_expire) {
msg << "can't be expired";
response = createAnswer(CONTROL_RESULT_EMPTY, msg.str());
} else {
msg << "expired";
response = createAnswer(CONTROL_RESULT_SUCCESS, msg.str());
}
}
handle.setArgument("response", response);
}
void
GssTsigImpl::keyDelHandler(CalloutHandle& handle) {
ConstElementPtr response;
string name;
ManagedKeyPtr key;
try {
// Command is always provided.
ConstElementPtr command;
handle.getArgument("command", command);
// Retrieve arguments.
ConstElementPtr args;
static_cast<void>(parseCommand(args, command));
// Arguments are required.
if (!args) {
isc_throw(BadValue, "arguments not found in the '"
<< command->str() << "' command");
}
// Arguments must be a map.
if (args->getType() != Element::map) {
isc_throw(BadValue, "arguments in the '" << command->str()
<< "' command are not a map");
}
// key-name is mandatory.
ConstElementPtr key_name = args->get("key-name");
if (!key_name) {
isc_throw(BadValue, "'key-name' is mandatory for the '"
<< command->str() << "' command");
}
// key-name must be a string.
if (key_name->getType() != Element::string) {
isc_throw(BadValue, "'key-name' must be a string in the '"
<< command->str() << "' command");
}
name = key_name->stringValue();
key = findKey(name);
} catch (const std::exception& ex) {
// There was an error while parsing command arguments.
// Return an error status code to notify the user.
response = createAnswer(CONTROL_RESULT_ERROR, ex.what());
handle.setArgument("response", response);
return;
}
ostringstream msg;
msg << "GSS-TSIG key '" << name << "' ";
// No key.
if (!key) {
msg << "not found";
response = createAnswer(CONTROL_RESULT_EMPTY, msg.str());
} else {
msg << "deleted";
auto it = keys_.find(name);
if (it != keys_.end()) {
key->getTKeyExchange().reset();
keys_.erase(it);
}
response = createAnswer(CONTROL_RESULT_SUCCESS, msg.str());
}
handle.setArgument("response", response);
}
void
GssTsigImpl::purgeHandler(CalloutHandle& handle) {
ConstElementPtr response;
string id;
try {
// Command is always provided.
ConstElementPtr command;
handle.getArgument("command", command);
// Retrieve arguments.
ConstElementPtr args;
static_cast<void>(parseCommand(args, command));
// Arguments are required.
if (!args) {
isc_throw(BadValue, "arguments not found in the '"
<< command->str() << "' command");
}
// Arguments must be a map.
if (args->getType() != Element::map) {
isc_throw(BadValue, "arguments in the '" << command->str()
<< "' command are not a map");
}
// server-id is mandatory.
ConstElementPtr server_id = args->get("server-id");
if (!server_id) {
isc_throw(BadValue, "'server-id' is mandatory for the '"
<< command->str() << "' command");
}
// server-id must be a string.
if (server_id->getType() != Element::string) {
isc_throw(BadValue, "'server-id' must be a string in the '"
<< command->str() << "' command");
}
id = server_id->stringValue();
} catch (const std::exception& ex) {
// There was an error while parsing command arguments.
// Return an error status code to notify the user.
response = createAnswer(CONTROL_RESULT_ERROR, ex.what());
handle.setArgument("response", response);
return;
}
// Build the list of keys to remove.
auto now = system_clock::now();
list<ManagedKeyPtr> to_purge;
auto const& idx = keys_.get<GssTsigKeyServerTag>();
auto const& range = idx.equal_range(id);
BOOST_FOREACH(auto const& key, range) {
lock_guard<mutex> lock(*key->mutex_);
auto status = key->getStatus();
switch (status) {
case ManagedKey::NOT_READY:
// skip
break;
case ManagedKey::USABLE:
if (now < key->getExpire()) {
// skip
break;
}
key->setStatus(ManagedKey::EXPIRED);
to_purge.push_back(key);
break;
default:
to_purge.push_back(key);
break;
}
}
// Remove all elements of the list.
for (auto const& key : to_purge) {
auto it = keys_.find(key->getKeyNameStr());
if (it != keys_.end()) {
key->getTKeyExchange().reset();
keys_.erase(it);
}
}
ostringstream msg;
msg << to_purge.size() << " purged keys for GSS-TSIG server[" << id << "]";
if (to_purge.empty()) {
response = createAnswer(CONTROL_RESULT_EMPTY, msg.str());
} else {
response = createAnswer(CONTROL_RESULT_SUCCESS, msg.str());
}
handle.setArgument("response", response);
}
void
GssTsigImpl::purgeAllHandler(CalloutHandle& handle) {
ConstElementPtr response;
// Build the list of keys to remove.
auto now = system_clock::now();
list<ManagedKeyPtr> to_purge;
for (auto const& key : keys_) {
lock_guard<mutex> lock(*key->mutex_);
auto status = key->getStatus();
switch (status) {
case ManagedKey::NOT_READY:
// skip
break;
case ManagedKey::USABLE:
if (now < key->getExpire()) {
// skip
break;
}
key->setStatus(ManagedKey::EXPIRED);
to_purge.push_back(key);
break;
default:
to_purge.push_back(key);
break;
}
}
// Remove all elements of the list.
for (auto const& key : to_purge) {
auto it = keys_.find(key->getKeyNameStr());
if (it != keys_.end()) {
key->getTKeyExchange().reset();
keys_.erase(it);
}
}
ostringstream msg;
msg << to_purge.size() << " purged GSS-TSIG keys";
if (to_purge.empty()) {
response = createAnswer(CONTROL_RESULT_EMPTY, msg.str());
} else {
response = createAnswer(CONTROL_RESULT_SUCCESS, msg.str());
}
handle.setArgument("response", response);
}
void
GssTsigImpl::rekeyHandler(CalloutHandle& handle) {
ConstElementPtr response;
string id;
DnsServerPtr server;
try {
// Command is always provided.
ConstElementPtr command;
handle.getArgument("command", command);
// Retrieve arguments.
ConstElementPtr args;
static_cast<void>(parseCommand(args, command));
// Arguments are required.
if (!args) {
isc_throw(BadValue, "arguments not found in the '"
<< command->str() << "' command");
}
// Arguments must be a map.
if (args->getType() != Element::map) {
isc_throw(BadValue, "arguments in the '" << command->str()
<< "' command are not a map");
}
// server-id is mandatory.
ConstElementPtr server_id = args->get("server-id");
if (!server_id) {
isc_throw(BadValue, "'server-id' is mandatory for the '"
<< command->str() << "' command");
}
// server-id must be a string.
if (server_id->getType() != Element::string) {
isc_throw(BadValue, "'server-id' must be a string in the '"
<< command->str() << "' command");
}
id = server_id->stringValue();
server = cfg_.getServer(id);
} catch (const std::exception& ex) {
// There was an error while parsing command arguments.
// Return an error status code to notify the user.
response = createAnswer(CONTROL_RESULT_ERROR, ex.what());
handle.setArgument("response", response);
return;
}
ostringstream msg;
msg << "GSS-TSIG server[" << id << "] ";
// No server.
if (!server) {
msg << "not found";
response = createAnswer(CONTROL_RESULT_EMPTY, msg.str());
} else {
auto now = system_clock::now();
createKey(server, now);
msg << "rekeyed";
response = createAnswer(CONTROL_RESULT_SUCCESS, msg.str());
}
handle.setArgument("response", response);
}
void
GssTsigImpl::rekeyAllHandler(CalloutHandle& handle) {
io_service_->post([this]() { processAllServersKeys(true); });
ConstElementPtr response = createAnswer(CONTROL_RESULT_SUCCESS, "rekeyed");
handle.setArgument("response", response);
}
void
GssTsigImpl::commandProcessed(CalloutHandle& handle) {
// Filter command names.
string command_name;
handle.getArgument("name", command_name);
if (command_name != "status-get") {
return;
}
// Get the response.
ConstElementPtr response;
handle.getArgument("response", response);
if (!response || (response->getType() != Element::map)) {
return;
}
// Get the arguments item from the response.
ConstElementPtr resp_args = response->get("arguments");
if (!resp_args || (resp_args->getType() != Element::map)) {
return;
}
// Add the gss-tsig entry.
ElementPtr mutable_resp_args = boost::const_pointer_cast<Element>(resp_args);
mutable_resp_args->set("gss-tsig", Element::create(true));
}
} // end of namespace isc::gss_tsig
} // end of namespace isc

View File

@ -0,0 +1,235 @@
// Copyright (C) 2021-2024 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 GSS_TSIG_IMPL_H
#define GSS_TSIG_IMPL_H
#include <asiolink/interval_timer.h>
#include <gss_tsig_cfg.h>
#include <hooks/hooks.h>
#include <managed_key.h>
#include <boost/multi_index/composite_key.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <chrono>
#include <memory>
namespace isc {
namespace gss_tsig {
/// @brief Tag for the name index for searching GSS-TSIG key.
struct GssTsigKeyNameTag { };
/// @brief Tag for the server ID index for searching GSS-TSIG key.
struct GssTsigKeyServerTag { };
/// @brief A multi index container holding GSS-TSIG keys.
typedef boost::multi_index_container<
// It holds pointers to GSS-TSIG key objects.
ManagedKeyPtr,
boost::multi_index::indexed_by<
// First index is the by DNS name.
boost::multi_index::hashed_unique<
boost::multi_index::tag<GssTsigKeyNameTag>,
boost::multi_index::const_mem_fun<ManagedKey, std::string,
&ManagedKey::getKeyNameStr>
>,
// Second index is by DNS server (parent) ID with the inception
// date to sort search results.
boost::multi_index::ordered_non_unique<
boost::multi_index::tag<GssTsigKeyServerTag>,
boost::multi_index::composite_key<
ManagedKey,
// The DNS server (parent) ID.
boost::multi_index::const_mem_fun<ManagedKey, std::string,
&ManagedKey::getParentID>,
// The inception date.
boost::multi_index::const_mem_fun<GssTsigKey,
std::chrono::system_clock::time_point,
&GssTsigKey::getInception>
>
>
>
> ManagedKeyList;
/// @brief GSS-TSIG hook implementation.
///
/// @note: currently only the configuration part: the runtime part will be
/// added later.
class GssTsigImpl {
public:
/// @brief Constructor.
GssTsigImpl();
/// @brief Destructor.
virtual ~GssTsigImpl();
/// @brief Configure.
///
/// @param config A map element with parameters.
/// @throw BadValue and similar exceptions on error.
void configure(isc::data::ConstElementPtr config);
/// @brief Finish configure.
///
/// @param d2_config D2 server running configuration.
void finishConfigure(isc::d2::D2CfgContextPtr d2_config);
/// @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;
}
/// @brief Get the DNS server from its ID.
///
/// @param id The server ID.
/// @return the DNS server or null if not found.
DnsServerPtr getServer(const std::string& id) const;
/// @brief Start method.
///
/// @note This method is called from the I/O context after
/// the d2_srv_configured callout.
void start();
/// @brief Stop method.
///
/// @note This method is called before unloading.
void stop();
/// @brief Process GSS-TSIG keys for all servers.
///
/// @param rekey The flag which indicates if unconditionally rekey all
/// servers.
void processAllServersKeys(bool rekey = false);
/// @brief Process GSS-TSIG keys for a specific server.
///
/// @param server The server with keys to be processed.
/// @param rekey The flag which indicates if unconditionally rekey server.
void processServerKeys(DnsServerPtr server, bool rekey = false);
/// @brief Create new GSS-TSIG key.
///
/// @param server The server for which the new key is added.
/// @param now The current timestamp.
void createKey(DnsServerPtr server,
std::chrono::time_point<std::chrono::system_clock> now);
/// @brief Find a GSS-TSIG key by server info.
///
/// @param server_info Pointer to a DNS server.
/// @param[out] useGssTsig Set to true when GSS-TSIG is used.
/// @param[out] fallback Set to true when GSS-TSIG should be used and
/// no key is available fallbacks to the no GSS-TSIG behavior (vs.
/// skips this DNS server).
/// @return an usable key or null if none was found.
ManagedKeyPtr findKey(const d2::DnsServerInfoPtr& server_info,
bool& useGssTsig, bool& fallback);
/// @brief Find a GSS-TSIG key by name.
///
/// @param name Name of the key.
/// @return the key with the name or null if none was found.
ManagedKeyPtr findKey(const std::string& name) const;
/// @brief Purge very old GSS-TSIG keys.
///
/// Handler of the purge periodic timer.
void purgeKeys();
/// @brief The gss-tsig-get command handler.
///
/// @param handle CalloutHandle.
void getHandler(isc::hooks::CalloutHandle& handle) const;
/// @brief The gss-tsig-get-all command handler.
///
/// @param handle CalloutHandle.
void getAllHandler(isc::hooks::CalloutHandle& handle) const;
/// @brief The gss-tsig-list command handler.
///
/// @param handle CalloutHandle.
void listHandler(isc::hooks::CalloutHandle& handle) const;
/// @brief The gss-tsig-key-get command handler.
///
/// @param handle CalloutHandle.
void keyGetHandler(isc::hooks::CalloutHandle& handle) const;
/// @brief The gss-tsig-key-expire command handler.
///
/// @param handle CalloutHandle.
void keyExpireHandler(isc::hooks::CalloutHandle& handle);
/// @brief The gss-tsig-key-del command handler.
///
/// @param handle CalloutHandle.
void keyDelHandler(isc::hooks::CalloutHandle& handle);
/// @brief The gss-tsig-purge command handler.
///
/// @param handle CalloutHandle.
void purgeHandler(isc::hooks::CalloutHandle& handle);
/// @brief The gss-tsig-purge-all command handler.
///
/// @param handle CalloutHandle.
void purgeAllHandler(isc::hooks::CalloutHandle& handle);
/// @brief The gss-tsig-rekey command handler.
///
/// @param handle CalloutHandle.
void rekeyHandler(isc::hooks::CalloutHandle& handle);
/// @brief The gss-tsig-rekey-all command handler.
///
/// @param handle CalloutHandle.
void rekeyAllHandler(isc::hooks::CalloutHandle& handle);
/// @brief The command_processed handler.
///
/// @param handle CalloutHandle.
void commandProcessed(isc::hooks::CalloutHandle& handle);
protected:
/// @brief GSS-TSIG hook configuration.
GssTsigCfg cfg_;
/// @brief Map of GSS-TSIG keys by name.
ManagedKeyList keys_;
/// @brief The hook I/O service.
isc::asiolink::IOServicePtr io_service_;
/// @brief The previous value of client key table environment variable.
std::unique_ptr<std::string> krb5_client_ktname_prev_;
/// @brief The previous value of credential cache environment variable.
std::unique_ptr<std::string> krb5ccname_prev_;
/// @brief The purge periodic timer.
isc::asiolink::IntervalTimerPtr purge_timer_;
};
/// @brief Type of pointer to a GSS-TSIG hook configuration.
typedef std::unique_ptr<GssTsigImpl> GssTsigImplPtr;
} // end of namespace isc::gss_tsig
} // end of namespace isc
#endif // GSS_TSIG_IMPL_H

View File

@ -0,0 +1,30 @@
// Copyright (C) 2021-2022 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 <gss_tsig_key.h>
using namespace isc;
using namespace isc::dns;
using namespace std;
namespace isc {
namespace gss_tsig {
GssTsigKey::GssTsigKey(const string& key_name, gss_ctx_id_t sec_ctx)
: D2TsigKey(key_name + "::gss-tsig"), sec_ctx_(new GssApiSecCtx(sec_ctx)) {
}
GssTsigKey::GssTsigKey(const string& key_name, const vector<uint8_t>& import)
: D2TsigKey(key_name + "::gss-tsig"), sec_ctx_(new GssApiSecCtx(import)) {
}
GssTsigKey::~GssTsigKey() {
}
} // end of namespace isc::gss_tsig
} // end of namespace isc

View File

@ -0,0 +1,120 @@
// Copyright (C) 2021-2022 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 GSS_TSIG_KEY_H
#define GSS_TSIG_KEY_H
#include <d2srv/d2_tsig_key.h>
#include <gss_tsig_api.h>
#include <boost/shared_ptr.hpp>
#include <chrono>
namespace isc {
namespace gss_tsig {
/// @brief GSS-TSIG extension of the D2 TSIG key class.
///
/// Implements a @c isc::d2::D2TsigKey derived class which can be
/// used as the value of D2TsigKeyPtr so with minimal or no update to the
/// DNS++ library. The class adds to the D2TsigKey base a GSS-API security
/// context maintaining 1::1 binding the key and it including for the
/// lifetime: e.g. to get a fresh GSS-API security context a fresh object
/// must be created.
class GssTsigKey : public d2::D2TsigKey {
public:
/// @brief Constructor.
///
/// @param key_name Domain name of the key.
/// @param sec_ctx Security context (can be 0).
GssTsigKey(const std::string& key_name,
gss_ctx_id_t sec_ctx = GSS_C_NO_CONTEXT);
/// @brief Constructor.
///
/// Use the gss_import_sec_context GSS-API function. This constructor
/// is expected to be used for restoring / importing a security context
/// saved on disk.
///
/// @param key_name Domain name of the key.
/// @param import Vector of byte representing the GSS-API security context.
GssTsigKey(const std::string& key_name,
const std::vector<uint8_t>& import);
/// @brief Destructor.
virtual ~GssTsigKey();
/// @brief Get the security context.
///
/// @note: By construction the sec_ctx_ pointer is never null but
/// the security context lifetime is the same as the key object.
///
/// @return The security context.
GssApiSecCtx& getSecCtx() {
return (*sec_ctx_);
}
/// @brief Get the key inception.
///
/// @return The key inception date.
std::chrono::system_clock::time_point getInception() const {
return (inception_);
}
/// @brief Get the key inception (32 bits).
///
/// @return The key inception date as a 32 bit unsigned.
uint32_t getInception32() const {
std::time_t inception = std::chrono::system_clock::to_time_t(inception_);
return (static_cast<uint32_t>(inception));
}
/// @brief Set the key inception.
///
/// @param inception The new key inception date.
void setInception(const std::chrono::system_clock::time_point& inception) {
inception_ = inception;
}
/// @brief Get the key expire.
///
/// @return The key expire date.
std::chrono::system_clock::time_point getExpire() const {
return (expire_);
}
/// @brief Get the key expire (32 bits).
///
/// @return The key expire date as a 32 bit unsigned.
uint32_t getExpire32() const {
std::time_t expire = std::chrono::system_clock::to_time_t(expire_);
return (static_cast<uint32_t>(expire));
}
/// @brief Set the key expire.
///
/// @param expire The new key expire date.
void setExpire(const std::chrono::system_clock::time_point& expire) {
expire_ = expire;
}
protected:
/// @brief GSS-API security context.
std::unique_ptr<GssApiSecCtx> sec_ctx_;
/// @brief The key inception date.
std::chrono::system_clock::time_point inception_;
/// @brief The key expire date.
std::chrono::system_clock::time_point expire_;
};
/// @brief Type of pointer to a GSS-TSIG key.
typedef boost::shared_ptr<GssTsigKey> GssTsigKeyPtr;
} // end of namespace isc::gss_tsig
} // end of namespace isc
#endif // GSS_TSIG_KEY_H

View File

@ -0,0 +1,16 @@
// Copyright (C) 2021-2022 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 <gss_tsig_log.h>
namespace isc {
namespace gss_tsig {
isc::log::Logger gss_tsig_logger("gss-tsig-hooks");
} // end of namespace isc::gss_tsig
} // end of namespace isc

View File

@ -0,0 +1,22 @@
// Copyright (C) 2021-2022 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 GSS_TSIG_LOG_H
#define GSS_TSIG_LOG_H
#include <log/logger_support.h>
#include <log/macros.h>
#include <gss_tsig_messages.h>
namespace isc {
namespace gss_tsig {
extern isc::log::Logger gss_tsig_logger;
} // end of namespace isc::gss_tsig
} // end of namespace isc
#endif

View File

@ -0,0 +1,119 @@
// File created from ../../../../src/hooks/d2/gss_tsig/gss_tsig_messages.mes
#include <cstddef>
#include <log/message_types.h>
#include <log/message_initializer.h>
namespace isc {
namespace gss_tsig {
extern const isc::log::MessageID BAD_CLIENT_CREDENTIALS = "BAD_CLIENT_CREDENTIALS";
extern const isc::log::MessageID GSS_TSIG_COMMAND_PROCESSED_FAILED = "GSS_TSIG_COMMAND_PROCESSED_FAILED";
extern const isc::log::MessageID GSS_TSIG_LOAD_FAILED = "GSS_TSIG_LOAD_FAILED";
extern const isc::log::MessageID GSS_TSIG_LOAD_OK = "GSS_TSIG_LOAD_OK";
extern const isc::log::MessageID GSS_TSIG_MANAGER_STARTED = "GSS_TSIG_MANAGER_STARTED";
extern const isc::log::MessageID GSS_TSIG_MANAGER_STOPPED = "GSS_TSIG_MANAGER_STOPPED";
extern const isc::log::MessageID GSS_TSIG_MANAGER_STOP_ERROR = "GSS_TSIG_MANAGER_STOP_ERROR";
extern const isc::log::MessageID GSS_TSIG_MANAGER_STOP_GENERAL_ERROR = "GSS_TSIG_MANAGER_STOP_GENERAL_ERROR";
extern const isc::log::MessageID GSS_TSIG_NEW_KEY = "GSS_TSIG_NEW_KEY";
extern const isc::log::MessageID GSS_TSIG_NEW_KEY_SETUP_FAILED = "GSS_TSIG_NEW_KEY_SETUP_FAILED";
extern const isc::log::MessageID GSS_TSIG_NEW_KEY_SETUP_SUCCEED = "GSS_TSIG_NEW_KEY_SETUP_SUCCEED";
extern const isc::log::MessageID GSS_TSIG_OLD_KEY_REMOVED = "GSS_TSIG_OLD_KEY_REMOVED";
extern const isc::log::MessageID GSS_TSIG_UNLOAD_OK = "GSS_TSIG_UNLOAD_OK";
extern const isc::log::MessageID GSS_TSIG_VERIFIED = "GSS_TSIG_VERIFIED";
extern const isc::log::MessageID GSS_TSIG_VERIFY_FAILED = "GSS_TSIG_VERIFY_FAILED";
extern const isc::log::MessageID KEY_LOOKUP_DISABLED = "KEY_LOOKUP_DISABLED";
extern const isc::log::MessageID KEY_LOOKUP_FOUND = "KEY_LOOKUP_FOUND";
extern const isc::log::MessageID KEY_LOOKUP_NONE = "KEY_LOOKUP_NONE";
extern const isc::log::MessageID KEY_PROCESSING_FAILED = "KEY_PROCESSING_FAILED";
extern const isc::log::MessageID KEY_PROCESSING_FAILED_UNSPECIFIED_ERROR = "KEY_PROCESSING_FAILED_UNSPECIFIED_ERROR";
extern const isc::log::MessageID START_REKEY_TIMER = "START_REKEY_TIMER";
extern const isc::log::MessageID START_RETRY_TIMER = "START_RETRY_TIMER";
extern const isc::log::MessageID TKEY_EXCHANGE_ANSWER_CLASS = "TKEY_EXCHANGE_ANSWER_CLASS";
extern const isc::log::MessageID TKEY_EXCHANGE_FAILED_TO_VERIFY = "TKEY_EXCHANGE_FAILED_TO_VERIFY";
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_EMPTY_IN_TOKEN = "TKEY_EXCHANGE_FAIL_EMPTY_IN_TOKEN";
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_EMPTY_OUT_TOKEN = "TKEY_EXCHANGE_FAIL_EMPTY_OUT_TOKEN";
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_EMPTY_RESPONSE = "TKEY_EXCHANGE_FAIL_EMPTY_RESPONSE";
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_IO_ERROR = "TKEY_EXCHANGE_FAIL_IO_ERROR";
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_IO_STOPPED = "TKEY_EXCHANGE_FAIL_IO_STOPPED";
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_IO_TIMEOUT = "TKEY_EXCHANGE_FAIL_IO_TIMEOUT";
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_NOT_SIGNED = "TKEY_EXCHANGE_FAIL_NOT_SIGNED";
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_NO_RDATA = "TKEY_EXCHANGE_FAIL_NO_RDATA";
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_NO_RESPONSE_ANSWER = "TKEY_EXCHANGE_FAIL_NO_RESPONSE_ANSWER";
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_NULL_RESPONSE = "TKEY_EXCHANGE_FAIL_NULL_RESPONSE";
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_RESPONSE_ERROR = "TKEY_EXCHANGE_FAIL_RESPONSE_ERROR";
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_TKEY_ERROR = "TKEY_EXCHANGE_FAIL_TKEY_ERROR";
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_TO_INIT = "TKEY_EXCHANGE_FAIL_TO_INIT";
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_WRONG_RESPONSE_ANSWER_COUNT = "TKEY_EXCHANGE_FAIL_WRONG_RESPONSE_ANSWER_COUNT";
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_WRONG_RESPONSE_ANSWER_TYPE = "TKEY_EXCHANGE_FAIL_WRONG_RESPONSE_ANSWER_TYPE";
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_WRONG_RESPONSE_OPCODE = "TKEY_EXCHANGE_FAIL_WRONG_RESPONSE_OPCODE";
extern const isc::log::MessageID TKEY_EXCHANGE_NOT_A_RESPONSE = "TKEY_EXCHANGE_NOT_A_RESPONSE";
extern const isc::log::MessageID TKEY_EXCHANGE_OUT_TOKEN_NOT_EMPTY = "TKEY_EXCHANGE_OUT_TOKEN_NOT_EMPTY";
extern const isc::log::MessageID TKEY_EXCHANGE_RDATA_COUNT = "TKEY_EXCHANGE_RDATA_COUNT";
extern const isc::log::MessageID TKEY_EXCHANGE_RECEIVE_MESSAGE = "TKEY_EXCHANGE_RECEIVE_MESSAGE";
extern const isc::log::MessageID TKEY_EXCHANGE_RESPONSE_TTL = "TKEY_EXCHANGE_RESPONSE_TTL";
extern const isc::log::MessageID TKEY_EXCHANGE_SEND_MESSAGE = "TKEY_EXCHANGE_SEND_MESSAGE";
extern const isc::log::MessageID TKEY_EXCHANGE_VALID = "TKEY_EXCHANGE_VALID";
extern const isc::log::MessageID TKEY_EXCHANGE_VERIFIED = "TKEY_EXCHANGE_VERIFIED";
} // namespace gss_tsig
} // namespace isc
namespace {
const char* values[] = {
"BAD_CLIENT_CREDENTIALS", "bad client credentials: %1",
"GSS_TSIG_COMMAND_PROCESSED_FAILED", "command_processed callout failed: %1.",
"GSS_TSIG_LOAD_FAILED", "GSS-TSIG hooks library failed to load: %1.",
"GSS_TSIG_LOAD_OK", "GSS-TSIG hooks library loaded successfully.",
"GSS_TSIG_MANAGER_STARTED", "hooks library GSS-TSIG key periodic manager started.",
"GSS_TSIG_MANAGER_STOPPED", "hooks library GSS-TSIG key periodic manager stopped.",
"GSS_TSIG_MANAGER_STOP_ERROR", "manager stop error: %1",
"GSS_TSIG_MANAGER_STOP_GENERAL_ERROR", "manager stop general error",
"GSS_TSIG_NEW_KEY", "new GSS-TSIG key '%1' was created.",
"GSS_TSIG_NEW_KEY_SETUP_FAILED", "new GSS-TSIG key '%1' setup failed: %2.",
"GSS_TSIG_NEW_KEY_SETUP_SUCCEED", "new GSS-TSIG key '%1' setup succeed.",
"GSS_TSIG_OLD_KEY_REMOVED", "%1 old GSS-TSIG keys were removed",
"GSS_TSIG_UNLOAD_OK", "GSS-TSIG hooks library unloaded successfully.",
"GSS_TSIG_VERIFIED", "GSS-TSIG verify successed.",
"GSS_TSIG_VERIFY_FAILED", "GSS-TSIG verify failed: %1.",
"KEY_LOOKUP_DISABLED", "hooks library lookup for a key: GSS-TSIG is not enabled for the current DNS server.",
"KEY_LOOKUP_FOUND", "hooks library lookup for a key: return GSS-TSIG key '%1'.",
"KEY_LOOKUP_NONE", "hooks library lookup for a key: found no usable key.",
"KEY_PROCESSING_FAILED", "The GSS-TKEY processing for server %1 failed because of an error: %2",
"KEY_PROCESSING_FAILED_UNSPECIFIED_ERROR", "The GSS-TKEY processing for server %1 failed because of an unspecified error",
"START_REKEY_TIMER", "started timer handling rekey for server %1 in %2 seconds.",
"START_RETRY_TIMER", "started timer handling retry for server %1 in %2 seconds.",
"TKEY_EXCHANGE_ANSWER_CLASS", "GSS-TKEY exchange received a response with answer class: %1.",
"TKEY_EXCHANGE_FAILED_TO_VERIFY", "GSS-TKEY exchange failed because the response failed to verify.",
"TKEY_EXCHANGE_FAIL_EMPTY_IN_TOKEN", "GSS-TKEY exchange failed because input token is empty.",
"TKEY_EXCHANGE_FAIL_EMPTY_OUT_TOKEN", "GSS-TKEY exchange failed because output token is empty.",
"TKEY_EXCHANGE_FAIL_EMPTY_RESPONSE", "GSS-TKEY exchange failed because the response is empty.",
"TKEY_EXCHANGE_FAIL_IO_ERROR", "GSS-TKEY exchange failed because of the IO error: %1.",
"TKEY_EXCHANGE_FAIL_IO_STOPPED", "GSS-TKEY exchange failed because the IO service was stopped.",
"TKEY_EXCHANGE_FAIL_IO_TIMEOUT", "GSS-TKEY exchange failed because of IO timeout.",
"TKEY_EXCHANGE_FAIL_NOT_SIGNED", "GSS-TKEY exchange failed because the response is not signed.",
"TKEY_EXCHANGE_FAIL_NO_RDATA", "GSS-TKEY exchange failed because the response contains no rdata.",
"TKEY_EXCHANGE_FAIL_NO_RESPONSE_ANSWER", "GSS-TKEY exchange failed because the response contains no answer.",
"TKEY_EXCHANGE_FAIL_NULL_RESPONSE", "GSS-TKEY exchange failed because the response is null.",
"TKEY_EXCHANGE_FAIL_RESPONSE_ERROR", "GSS-TKEY exchange failed because the response contains an error: %1.",
"TKEY_EXCHANGE_FAIL_TKEY_ERROR", "GSS-TKEY exchange failed because the response contains TKEY error: %1.",
"TKEY_EXCHANGE_FAIL_TO_INIT", "GSS-TKEY exchange failed to initialize because of the error: %1.",
"TKEY_EXCHANGE_FAIL_WRONG_RESPONSE_ANSWER_COUNT", "GSS-TKEY exchange failed because the response contains invalid number of RRs: %1.",
"TKEY_EXCHANGE_FAIL_WRONG_RESPONSE_ANSWER_TYPE", "GSS-TKEY exchange failed because the response contains wrong answer type: %1.",
"TKEY_EXCHANGE_FAIL_WRONG_RESPONSE_OPCODE", "GSS-TKEY exchange failed because the response contains invalid opcode: %1.",
"TKEY_EXCHANGE_NOT_A_RESPONSE", "GSS-TKEY exchange received a non response type.",
"TKEY_EXCHANGE_OUT_TOKEN_NOT_EMPTY", "GSS-TKEY exchange output token is not empty.",
"TKEY_EXCHANGE_RDATA_COUNT", "GSS-TKEY exchange received a response with rdata count: %1.",
"TKEY_EXCHANGE_RECEIVE_MESSAGE", "GSS-TKEY exchange receives a message of size: %1.",
"TKEY_EXCHANGE_RESPONSE_TTL", "GSS-TKEY exchange received a response with TTL of: %1 seconds.",
"TKEY_EXCHANGE_SEND_MESSAGE", "GSS-TKEY exchange sends a message of size: %1.",
"TKEY_EXCHANGE_VALID", "GSS-TKEY exchange retrieved a TKEY valid for: %1 seconds.",
"TKEY_EXCHANGE_VERIFIED", "GSS-TKEY exchange verified.",
NULL
};
const isc::log::MessageInitializer initializer(values);
} // Anonymous namespace

View File

@ -0,0 +1,63 @@
// File created from ../../../../src/hooks/d2/gss_tsig/gss_tsig_messages.mes
#ifndef GSS_TSIG_MESSAGES_H
#define GSS_TSIG_MESSAGES_H
#include <log/message_types.h>
namespace isc {
namespace gss_tsig {
extern const isc::log::MessageID BAD_CLIENT_CREDENTIALS;
extern const isc::log::MessageID GSS_TSIG_COMMAND_PROCESSED_FAILED;
extern const isc::log::MessageID GSS_TSIG_LOAD_FAILED;
extern const isc::log::MessageID GSS_TSIG_LOAD_OK;
extern const isc::log::MessageID GSS_TSIG_MANAGER_STARTED;
extern const isc::log::MessageID GSS_TSIG_MANAGER_STOPPED;
extern const isc::log::MessageID GSS_TSIG_MANAGER_STOP_ERROR;
extern const isc::log::MessageID GSS_TSIG_MANAGER_STOP_GENERAL_ERROR;
extern const isc::log::MessageID GSS_TSIG_NEW_KEY;
extern const isc::log::MessageID GSS_TSIG_NEW_KEY_SETUP_FAILED;
extern const isc::log::MessageID GSS_TSIG_NEW_KEY_SETUP_SUCCEED;
extern const isc::log::MessageID GSS_TSIG_OLD_KEY_REMOVED;
extern const isc::log::MessageID GSS_TSIG_UNLOAD_OK;
extern const isc::log::MessageID GSS_TSIG_VERIFIED;
extern const isc::log::MessageID GSS_TSIG_VERIFY_FAILED;
extern const isc::log::MessageID KEY_LOOKUP_DISABLED;
extern const isc::log::MessageID KEY_LOOKUP_FOUND;
extern const isc::log::MessageID KEY_LOOKUP_NONE;
extern const isc::log::MessageID KEY_PROCESSING_FAILED;
extern const isc::log::MessageID KEY_PROCESSING_FAILED_UNSPECIFIED_ERROR;
extern const isc::log::MessageID START_REKEY_TIMER;
extern const isc::log::MessageID START_RETRY_TIMER;
extern const isc::log::MessageID TKEY_EXCHANGE_ANSWER_CLASS;
extern const isc::log::MessageID TKEY_EXCHANGE_FAILED_TO_VERIFY;
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_EMPTY_IN_TOKEN;
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_EMPTY_OUT_TOKEN;
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_EMPTY_RESPONSE;
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_IO_ERROR;
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_IO_STOPPED;
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_IO_TIMEOUT;
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_NOT_SIGNED;
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_NO_RDATA;
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_NO_RESPONSE_ANSWER;
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_NULL_RESPONSE;
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_RESPONSE_ERROR;
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_TKEY_ERROR;
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_TO_INIT;
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_WRONG_RESPONSE_ANSWER_COUNT;
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_WRONG_RESPONSE_ANSWER_TYPE;
extern const isc::log::MessageID TKEY_EXCHANGE_FAIL_WRONG_RESPONSE_OPCODE;
extern const isc::log::MessageID TKEY_EXCHANGE_NOT_A_RESPONSE;
extern const isc::log::MessageID TKEY_EXCHANGE_OUT_TOKEN_NOT_EMPTY;
extern const isc::log::MessageID TKEY_EXCHANGE_RDATA_COUNT;
extern const isc::log::MessageID TKEY_EXCHANGE_RECEIVE_MESSAGE;
extern const isc::log::MessageID TKEY_EXCHANGE_RESPONSE_TTL;
extern const isc::log::MessageID TKEY_EXCHANGE_SEND_MESSAGE;
extern const isc::log::MessageID TKEY_EXCHANGE_VALID;
extern const isc::log::MessageID TKEY_EXCHANGE_VERIFIED;
} // namespace gss_tsig
} // namespace isc
#endif // GSS_TSIG_MESSAGES_H

View File

@ -0,0 +1,222 @@
# Copyright (C) 2021-2024 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/.
$NAMESPACE isc::gss_tsig
% BAD_CLIENT_CREDENTIALS bad client credentials: %1
This error message is issued when the client credential processing failed,
including when the credential remaining lifetime is shorter than the
TKEY lifetime. The argument details the error.
% GSS_TSIG_COMMAND_PROCESSED_FAILED command_processed callout failed: %1.
This error message is issued when the callout for the command_processed
callout point failed. The argument contains a reason for the error.
% GSS_TSIG_LOAD_FAILED GSS-TSIG hooks library failed to load: %1.
This error message indicates that an error occurred attempting to
load the GSS-TSIG hooks library. The argument details the error.
% GSS_TSIG_LOAD_OK GSS-TSIG hooks library loaded successfully.
This info message indicates that the GSS-TSIG hooks library has
been loaded successfully.
% GSS_TSIG_MANAGER_STARTED hooks library GSS-TSIG key periodic manager started.
Logged at debug log level 40.
This debug message is issued when the GSS-TSIG key periodic manager has started.
% GSS_TSIG_MANAGER_STOPPED hooks library GSS-TSIG key periodic manager stopped.
Logged at debug log level 40.
This debug message is issued when the GSS-TSIG key periodic manager has stopped.
% GSS_TSIG_MANAGER_STOP_ERROR manager stop error: %1
This error message is issued when the GSS-TSIG key periodic manager has stopped
but an error is detected. The error message in the argument gives details about
the problem.
% GSS_TSIG_MANAGER_STOP_GENERAL_ERROR manager stop general error
This error message is issued when the GSS-TSIG key periodic manager has stopped
but a general error is detected.
% GSS_TSIG_NEW_KEY new GSS-TSIG key '%1' was created.
Logged at debug log level 40.
This info message indicates that the GSS-TSIG hooks library has
created a new GSS-TSIG key. The name of the new key is displayed.
% GSS_TSIG_NEW_KEY_SETUP_FAILED new GSS-TSIG key '%1' setup failed: %2.
This warning message is issued when the setup of a new GSS-TSIG key failed.
The name of the new key and the error are displayed.
% GSS_TSIG_NEW_KEY_SETUP_SUCCEED new GSS-TSIG key '%1' setup succeed.
Logged at debug log level 40.
This debug message is issued when the setup of a new GSS-TSIG key
successfully finished. The name of the new key is displayed.
% GSS_TSIG_OLD_KEY_REMOVED %1 old GSS-TSIG keys were removed
Logged at debug log level 40.
This debug message is issued when some old keys (older than 2 times the maximum
TKEY lifetime) were removed. The number of removed keys is displayed.
% GSS_TSIG_UNLOAD_OK GSS-TSIG hooks library unloaded successfully.
This info message indicates that the GSS-TSIG hooks library has
been unloaded successfully.
% GSS_TSIG_VERIFIED GSS-TSIG verify successed.
Logged at debug log level 40.
A debug message issued when GSS-TSIG verification succeeded.
% GSS_TSIG_VERIFY_FAILED GSS-TSIG verify failed: %1.
This info message indicates that GSS-TSIG verification failed.
The argument details the error.
% KEY_LOOKUP_DISABLED hooks library lookup for a key: GSS-TSIG is not enabled for the current DNS server.
Logged at debug log level 40.
This debug message is issued when the lookup for a GSS-TSIG key was performed
for a DNS server where GSS-TSIG is not enabled.
% KEY_LOOKUP_FOUND hooks library lookup for a key: return GSS-TSIG key '%1'.
Logged at debug log level 40.
This debug message is issued when the lookup for a GSS-TSIG key returned
an usable key for protecting the DNS update. The key name is displayed.
% KEY_LOOKUP_NONE hooks library lookup for a key: found no usable key.
Logged at debug log level 40.
This debug message is issued when the lookup for a GSS-TSIG key failed
to find an usable key.
% KEY_PROCESSING_FAILED The GSS-TKEY processing for server %1 failed because of an error: %2
This error message is issued when the key processing for a specific server has
failed. The first argument specifies the server identifier and the second
argument gives more information about the error.
% KEY_PROCESSING_FAILED_UNSPECIFIED_ERROR The GSS-TKEY processing for server %1 failed because of an unspecified error
This error message is issued when the key processing for a specific server has
failed. The first argument specifies the server identifier.
% START_REKEY_TIMER started timer handling rekey for server %1 in %2 seconds.
Logged at debug log level 40.
This debug message is issued when starting the rekey timer to handle new keys
for this server when at least one key is currently available. The first argument
specifies the server identifier and the second argument specifies the time
interval when the next key processing will be attempted.
% START_RETRY_TIMER started timer handling retry for server %1 in %2 seconds.
Logged at debug log level 40.
This debug message is issued when starting the retry timer to handle new keys
for this server when there is no key currently available. The first argument
specifies the server identifier and the second argument specifies the time
interval when the next key processing will be attempted.
% TKEY_EXCHANGE_ANSWER_CLASS GSS-TKEY exchange received a response with answer class: %1.
Logged at debug log level 40.
This debug message indicates that GSS-TKEY exchange received a response with
specified answer class.
% TKEY_EXCHANGE_FAILED_TO_VERIFY GSS-TKEY exchange failed because the response failed to verify.
This error message indicated that GSS-TKEY exchange failed because the response
failed to verify.
% TKEY_EXCHANGE_FAIL_EMPTY_IN_TOKEN GSS-TKEY exchange failed because input token is empty.
This error message indicated that GSS-TKEY exchange failed because input token
is empty.
% TKEY_EXCHANGE_FAIL_EMPTY_OUT_TOKEN GSS-TKEY exchange failed because output token is empty.
This error message indicated that GSS-TKEY exchange failed because output token
is empty.
% TKEY_EXCHANGE_FAIL_EMPTY_RESPONSE GSS-TKEY exchange failed because the response is empty.
This error message indicated that GSS-TKEY exchange failed because the response
is empty.
% TKEY_EXCHANGE_FAIL_IO_ERROR GSS-TKEY exchange failed because of the IO error: %1.
This error message indicated that GSS-TKEY exchange failed because of an IO error.
The argument details the IO error.
% TKEY_EXCHANGE_FAIL_IO_STOPPED GSS-TKEY exchange failed because the IO service was stopped.
This error message indicated that GSS-TKEY exchange failed because the IO
service was stopped.
% TKEY_EXCHANGE_FAIL_IO_TIMEOUT GSS-TKEY exchange failed because of IO timeout.
This error message indicated that GSS-TKEY exchange failed because of IO
timeout.
% TKEY_EXCHANGE_FAIL_NOT_SIGNED GSS-TKEY exchange failed because the response is not signed.
This error message indicated that GSS-TKEY exchange failed because the response
is not signed.
% TKEY_EXCHANGE_FAIL_NO_RDATA GSS-TKEY exchange failed because the response contains no rdata.
This error message indicated that GSS-TKEY exchange failed because the response
contains no rdata.
% TKEY_EXCHANGE_FAIL_NO_RESPONSE_ANSWER GSS-TKEY exchange failed because the response contains no answer.
This error message indicated that GSS-TKEY exchange failed because the response
contains no answer.
% TKEY_EXCHANGE_FAIL_NULL_RESPONSE GSS-TKEY exchange failed because the response is null.
This error message indicated that GSS-TKEY exchange failed because the response
is null.
% TKEY_EXCHANGE_FAIL_RESPONSE_ERROR GSS-TKEY exchange failed because the response contains an error: %1.
This error message indicated that GSS-TKEY exchange failed because the response
contains an error. The argument details the reponse error.
% TKEY_EXCHANGE_FAIL_TKEY_ERROR GSS-TKEY exchange failed because the response contains TKEY error: %1.
This error message indicated that GSS-TKEY exchange failed because the response
contains TKEY error. The argument details the TKEY error.
% TKEY_EXCHANGE_FAIL_TO_INIT GSS-TKEY exchange failed to initialize because of the error: %1.
This error message indicated that GSS-TKEY exchange failed in the
initialization phase, for instance because the server principal does not
exist. The argument details the error.
% TKEY_EXCHANGE_FAIL_WRONG_RESPONSE_ANSWER_COUNT GSS-TKEY exchange failed because the response contains invalid number of RRs: %1.
This error message indicated that GSS-TKEY exchange failed because the response
contains invalid number of RRs. The argument contains the wrong number of RRs.
% TKEY_EXCHANGE_FAIL_WRONG_RESPONSE_ANSWER_TYPE GSS-TKEY exchange failed because the response contains wrong answer type: %1.
This error message indicated that GSS-TKEY exchange failed because the response
contains wrong answer type. The argument contains the wrong answer type.
% TKEY_EXCHANGE_FAIL_WRONG_RESPONSE_OPCODE GSS-TKEY exchange failed because the response contains invalid opcode: %1.
This error message indicated that GSS-TKEY exchange failed because the response
contains invalid opcode. The argument contains the wrong opcode.
% TKEY_EXCHANGE_NOT_A_RESPONSE GSS-TKEY exchange received a non response type.
Logged at debug log level 40.
This debug message indicates that GSS-TKEY exchange received a non response
type.
% TKEY_EXCHANGE_OUT_TOKEN_NOT_EMPTY GSS-TKEY exchange output token is not empty.
Logged at debug log level 40.
This debug message indicates that GSS-TKEY exchange output token is not empty.
% TKEY_EXCHANGE_RDATA_COUNT GSS-TKEY exchange received a response with rdata count: %1.
Logged at debug log level 40.
This debug message indicates that GSS-TKEY exchange received a response with
specified rdata count.
% TKEY_EXCHANGE_RECEIVE_MESSAGE GSS-TKEY exchange receives a message of size: %1.
Logged at debug log level 40.
This debug message indicates that GSS-TKEY exchange receives a message of
specified size.
% TKEY_EXCHANGE_RESPONSE_TTL GSS-TKEY exchange received a response with TTL of: %1 seconds.
Logged at debug log level 40.
This debug message indicates that GSS-TKEY exchange received a response with
specified TTL.
% TKEY_EXCHANGE_SEND_MESSAGE GSS-TKEY exchange sends a message of size: %1.
Logged at debug log level 40.
This debug message indicates that GSS-TKEY exchange sends a message of specified
size.
% TKEY_EXCHANGE_VALID GSS-TKEY exchange retrieved a TKEY valid for: %1 seconds.
Logged at debug log level 40.
This debug message indicates that GSS-TKEY exchange retrieved a TKEY valid for
the specified time period expressed in seconds.
% TKEY_EXCHANGE_VERIFIED GSS-TKEY exchange verified.
Logged at debug log level 40.
This debug message indicates that GSS-TKEY exchange is verified.

View File

@ -0,0 +1 @@
libddns_gss_tsig_unittests

View File

@ -0,0 +1,64 @@
SUBDIRS = .
AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
AM_CPPFLAGS += -I$(top_builddir)/src/hooks/d2/gss_tsig -I$(top_srcdir)/src/hooks/d2/gss_tsig
AM_CPPFLAGS += $(GSSAPI_CFLAGS) $(BOOST_INCLUDES)
AM_CPPFLAGS += $(CRYPTO_CFLAGS) $(CRYPTO_INCLUDES)
AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(abs_top_builddir)/src/hooks/d2/gss_tsig/libloadtests\"
AM_CPPFLAGS += -DLIBDHCP_GSS_TSIG_SO=\"$(abs_top_builddir)/src/hooks/d2/gss_tsig/.libs/libddns_gss_tsig.so\"
AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\"
AM_CXXFLAGS = $(KEA_CXXFLAGS)
# Some versions of GCC warn about some versions of Boost regarding
# missing initializer for members in its posix_time.
# https://svn.boost.org/trac/boost/ticket/3477
# But older GCC compilers don't have the flag.
AM_CXXFLAGS += $(WARNING_NO_MISSING_FIELD_INITIALIZERS_CFLAG)
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)
TESTS =
if HAVE_GTEST
TESTS += libddns_gss_tsig_unittests
libddns_gss_tsig_unittests_SOURCES = run_unittests.cc
libddns_gss_tsig_unittests_SOURCES += load_unload_unittests.cc
libddns_gss_tsig_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) $(LOG4CPLUS_INCLUDES)
libddns_gss_tsig_unittests_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) $(GTEST_LDFLAGS)
libddns_gss_tsig_unittests_CXXFLAGS = $(AM_CXXFLAGS)
libddns_gss_tsig_unittests_LDADD = $(top_builddir)/src/lib/d2srv/libkea-d2srv.la
libddns_gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/process/libkea-process.la
libddns_gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la
libddns_gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/asiodns/libkea-asiodns.la
libddns_gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la
libddns_gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la
libddns_gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la
libddns_gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/testutils/libkea-testutils.la
libddns_gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la
libddns_gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
libddns_gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/dns/libkea-dns++.la
libddns_gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la
libddns_gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la
libddns_gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/util/libkea-util.la
libddns_gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
libddns_gss_tsig_unittests_LDADD += $(GSSAPI_LIBS)
libddns_gss_tsig_unittests_LDADD += $(LOG4CPLUS_LIBS)
libddns_gss_tsig_unittests_LDADD += $(CRYPTO_LIBS)
libddns_gss_tsig_unittests_LDADD += $(BOOST_LIBS)
libddns_gss_tsig_unittests_LDADD += $(GTEST_LDADD)
endif
noinst_PROGRAMS = $(TESTS)

View File

@ -0,0 +1,387 @@
// Copyright (C) 2021-2024 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 high availability 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 <asiolink/io_service.h>
#include <cc/command_interpreter.h>
#include <d2srv/d2_cfg_mgr.h>
#include <exceptions/exceptions.h>
#include <hooks/hooks.h>
#include <hooks/hooks_manager.h>
#include <process/daemon.h>
#include <testutils/gtest_utils.h>
#include <gtest/gtest.h>
#include <errno.h>
using namespace std;
using namespace isc;
using namespace isc::asiolink;
using namespace isc::config;
using namespace isc::d2;
using namespace isc::data;
using namespace isc::hooks;
using namespace isc::process;
namespace {
/// @brief Structure that holds registered hook indexes.
struct TestHooks {
/// @brief Index of d2_srv_configured callout.
int hook_index_d2_srv_configured_;
/// @brief Constructor
///
/// The constructor registers hook points for callout tests.
TestHooks() {
hook_index_d2_srv_configured_ =
HooksManager::registerHook("d2_srv_configured");
}
};
TestHooks testHooks;
/// @brief Test fixture for testing loading and unloading the GSS-TSIG library.
class LibLoadTest : public ::testing::Test {
public:
/// @brief Constructor.
LibLoadTest() {
reset();
}
/// @brief Destructor.
/// Removes files that may be left over from previous tests.
virtual ~LibLoadTest() {
reset();
}
/// @brief Removes files that may be left over from previous tests.
virtual void reset() {
HooksManager::unloadLibraries();
}
/// @brief Adds library/parameters to list of libraries to be loaded.
void addLib(const std::string& lib, ConstElementPtr params) {
libraries_.push_back(make_pair(lib, params));
}
/// @brief Load all specified libraries.
///
/// The libraries are stored in libraries_
bool loadLibs() {
return (HooksManager::loadLibraries(libraries_));
}
/// @brief Unloads all libraries.
void unloadLibs() {
EXPECT_NO_THROW(HooksManager::unloadLibraries());
}
/// @brief Return basic, valid GSS-TSIG configuration in JSON format.
ElementPtr createValidJsonConfiguration() const {
std::string config_text = "{ }";
return (Element::fromJSON(config_text));
}
/// @brief Libraries.
HookLibsCollection libraries_;
};
// Simple test that checks the library can be loaded in a D2 server.
TEST_F(LibLoadTest, validLoad) {
// Set proc name.
Daemon::setProcName("kea-dhcp-ddns");
// Add library with valid configuration.
addLib(LIBDHCP_GSS_TSIG_SO, createValidJsonConfiguration());
// Library should load without issue.
EXPECT_TRUE(loadLibs());
}
// Simple test that checks the library can be loaded and unloaded several times
// in a D2 server.
TEST_F(LibLoadTest, validLoads) {
// Set proc name.
Daemon::setProcName("kea-dhcp-ddns");
// Add library with valid configuration.
addLib(LIBDHCP_GSS_TSIG_SO, createValidJsonConfiguration());
EXPECT_TRUE(loadLibs());
unloadLibs();
EXPECT_TRUE(loadLibs());
unloadLibs();
}
// Verifies that an unknown parameter in an otherwise valid configuration
// is detected.
TEST_F(LibLoadTest, unknownParameterLoad) {
// Set proc name.
Daemon::setProcName("kea-dhcp-ddns");
/// Add unknown element "foo" to valid config.
ElementPtr config = createValidJsonConfiguration();
config->set("foo", Element::create("bar"));
// Add library with invalid configuration
addLib(LIBDHCP_GSS_TSIG_SO, config);
// The load should fail.
EXPECT_FALSE(loadLibs());
}
// Verifies that a bad type parameter in an otherwise valid configuration
// is detected.
TEST_F(LibLoadTest, badTypeParameterLoad) {
// Set proc name.
Daemon::setProcName("kea-dhcp-ddns");
/// Add servers with bad type to valid config.
ElementPtr config = createValidJsonConfiguration();
config->set("servers", Element::createMap());
// Add library with invalid configuration
addLib(LIBDHCP_GSS_TSIG_SO, config);
// The load should fail.
EXPECT_FALSE(loadLibs());
}
// Verifies that a bad parameter in an otherwise valid configuration
// is detected.
TEST_F(LibLoadTest, badParameterLoad) {
// Set proc name.
Daemon::setProcName("kea-dhcp-ddns");
/// Add tkey-protocol with bad value to valid config.
ElementPtr config = createValidJsonConfiguration();
config->set("tkey-protocol", Element::create("FOO"));
// Add library with invalid configuration
addLib(LIBDHCP_GSS_TSIG_SO, config);
// The load should fail.
EXPECT_FALSE(loadLibs());
}
// Verifies that invalid d2_srv_configured callout d2_config parameter fail.
TEST_F(LibLoadTest, badD2ConfigCallout) {
// Set proc name.
Daemon::setProcName("kea-dhcp-ddns");
// Add library with valid configuration.
addLib(LIBDHCP_GSS_TSIG_SO, createValidJsonConfiguration());
// Load the library.
ASSERT_TRUE(loadLibs());
// Get and setup the callout handle.
EXPECT_TRUE(HooksManager::calloutsPresent(testHooks.hook_index_d2_srv_configured_));
CalloutHandlePtr handle = HooksManager::createCalloutHandle();
handle->setStatus(CalloutHandle::NEXT_STEP_CONTINUE);
D2CfgContextPtr d2_config;
handle->setArgument("server_config", d2_config);
// Execute the callout (the exception is not propagated).
EXPECT_NO_THROW(HooksManager::callCallouts(testHooks.hook_index_d2_srv_configured_,
*handle));
// Expect an exception logged with error message:
// gss_tsig d2_srv_configured: server_config is null
}
// Verifies that valid and empty config works with d2_srv_configured callout.
TEST_F(LibLoadTest, emptyCallout) {
// Set proc name.
Daemon::setProcName("kea-dhcp-ddns");
// Add library with valid configuration.
addLib(LIBDHCP_GSS_TSIG_SO, createValidJsonConfiguration());
// Load the library.
ASSERT_TRUE(loadLibs());
// Get and setup the callout handle.
EXPECT_TRUE(HooksManager::calloutsPresent(testHooks.hook_index_d2_srv_configured_));
CalloutHandlePtr handle = HooksManager::createCalloutHandle();
handle->setStatus(CalloutHandle::NEXT_STEP_CONTINUE);
D2CfgContextPtr d2_config(new D2CfgContext());
handle->setArgument("server_config", d2_config);
// Execute the callout.
EXPECT_NO_THROW(HooksManager::callCallouts(testHooks.hook_index_d2_srv_configured_,
*handle));
// The configuration was accepted.
EXPECT_EQ(CalloutHandle::NEXT_STEP_CONTINUE, handle->getStatus());
}
// Verifies that mismatch between hook config and empty d2 config
// is detected.
TEST_F(LibLoadTest, badEmptyCallout) {
// Create a hook config.
string config = "{\n"
"\"server-principal\": \"DNS/server.example.org@REALM\",\n"
"\"servers\": [ {\n"
" \"id\": \"foo\",\n"
" \"ip-address\": \"192.0.2.1\"\n"
" } ] }\n";
ElementPtr json;
ASSERT_NO_THROW(json = Element::fromJSON(config));
// Set proc name.
Daemon::setProcName("kea-dhcp-ddns");
// Add library with configuration.
addLib(LIBDHCP_GSS_TSIG_SO, json);
// Load the library.
ASSERT_TRUE(loadLibs());
// Get and setup the callout handle.
EXPECT_TRUE(HooksManager::calloutsPresent(testHooks.hook_index_d2_srv_configured_));
CalloutHandlePtr handle = HooksManager::createCalloutHandle();
handle->setStatus(CalloutHandle::NEXT_STEP_CONTINUE);
D2CfgContextPtr d2_config(new D2CfgContext());
handle->setArgument("server_config", d2_config);
// Execute the callout.
EXPECT_NO_THROW(HooksManager::callCallouts(testHooks.hook_index_d2_srv_configured_,
*handle));
// The configuration was rejected.
EXPECT_EQ(CalloutHandle::NEXT_STEP_DROP, handle->getStatus());
string expected = "gss_tsig config mismatch: server info can't be found";
string error;
handle->getArgument("error", error);
EXPECT_EQ(expected, error);
}
// Verifies that mismatch between hook config and d2 config
// is detected.
TEST_F(LibLoadTest, badCallout) {
// D2 config with not matching server.
string d2_config = "{\n"
"\"forward-ddns\": {\n"
" \"ddns-domains\": [ {\n"
" \"name\": \"foo.example.com.\",\n"
" \"dns-servers\": [ {\n"
" \"ip-address\": \"192.1.2.3\"\n"
" } ] } ] } }\n";
ConstElementPtr d2_json;
ASSERT_NO_THROW(d2_json = Element::fromJSON(d2_config));
D2CfgMgr mgr;
ConstElementPtr answer = mgr.simpleParseConfig(d2_json);
int rcode = -1;
ConstElementPtr comment = parseAnswer(rcode, answer);
if (rcode != CONTROL_RESULT_SUCCESS) {
FAIL() << "d2 config parse failed: " << comment->str();
}
D2CfgContextPtr d2_ctx = mgr.getD2CfgContext();
ASSERT_TRUE(d2_ctx);
// Create a hook config.
string config = "{\n"
"\"server-principal\": \"DNS/server.example.org@REALM\",\n"
"\"servers\": [ {\n"
" \"id\": \"foo\",\n"
" \"ip-address\": \"192.0.2.1\"\n"
" } ] }\n";
ElementPtr json;
ASSERT_NO_THROW(json = Element::fromJSON(config));
// Set proc name.
Daemon::setProcName("kea-dhcp-ddns");
// Add library with configuration.
addLib(LIBDHCP_GSS_TSIG_SO, json);
// Load the library.
ASSERT_TRUE(loadLibs());
// Get and setup the callout handle.
EXPECT_TRUE(HooksManager::calloutsPresent(testHooks.hook_index_d2_srv_configured_));
CalloutHandlePtr handle = HooksManager::createCalloutHandle();
handle->setStatus(CalloutHandle::NEXT_STEP_CONTINUE);
handle->setArgument("server_config", d2_ctx);
// Execute the callout.
EXPECT_NO_THROW(HooksManager::callCallouts(testHooks.hook_index_d2_srv_configured_,
*handle));
// The configuration was rejected.
EXPECT_EQ(CalloutHandle::NEXT_STEP_DROP, handle->getStatus());
string expected = "gss_tsig config mismatch: server info can't be found";
string error;
handle->getArgument("error", error);
EXPECT_EQ(expected, error);
}
// Verifies that hook config and d2 config work.
TEST_F(LibLoadTest, callout) {
// D2 config with matching server.
string d2_config = "{\n"
"\"forward-ddns\": {\n"
" \"ddns-domains\": [ {\n"
" \"name\": \"foo.example.com.\",\n"
" \"dns-servers\": [ {\n"
" \"ip-address\": \"192.0.2.1\"\n"
" } ] } ] } }\n";
ConstElementPtr d2_json;
ASSERT_NO_THROW(d2_json = Element::fromJSON(d2_config));
D2CfgMgr mgr;
ConstElementPtr answer = mgr.simpleParseConfig(d2_json);
int rcode = -1;
ConstElementPtr comment = parseAnswer(rcode, answer);
if (rcode != CONTROL_RESULT_SUCCESS) {
FAIL() << "d2 config parse failed: " << comment->str();
}
D2CfgContextPtr d2_ctx = mgr.getD2CfgContext();
ASSERT_TRUE(d2_ctx);
// Create a hook config.
string config = "{\n"
"\"server-principal\": \"DNS/server.example.org@REALM\",\n"
"\"servers\": [ {\n"
" \"id\": \"foo\",\n"
" \"ip-address\": \"192.0.2.1\"\n"
" } ] }\n";
ElementPtr json;
ASSERT_NO_THROW(json = Element::fromJSON(config));
// Set proc name.
Daemon::setProcName("kea-dhcp-ddns");
// Add library with configuration.
addLib(LIBDHCP_GSS_TSIG_SO, json);
// Load the library.
ASSERT_TRUE(loadLibs());
// Get and setup the callout handle.
EXPECT_TRUE(HooksManager::calloutsPresent(testHooks.hook_index_d2_srv_configured_));
CalloutHandlePtr handle = HooksManager::createCalloutHandle();
handle->setStatus(CalloutHandle::NEXT_STEP_CONTINUE);
handle->setArgument("server_config", d2_ctx);
// Execute the callout.
EXPECT_NO_THROW(HooksManager::callCallouts(testHooks.hook_index_d2_srv_configured_,
*handle));
// The configuration was accepted.
EXPECT_EQ(CalloutHandle::NEXT_STEP_CONTINUE, handle->getStatus());
}
} // end of anonymous namespace

View File

@ -0,0 +1,18 @@
if not gtest.found()
subdir_done()
endif
current_source_dir = meson.current_source_dir()
ddns_gss_tsig_libloadtests = executable(
'ddns-gss-tsig-libload-tests',
'load_unload_unittests.cc',
'run_unittests.cc',
cpp_args: [
f'-DTEST_DATA_BUILDDIR="@current_source_dir@"',
f'-DLIBDHCP_GSS_TSIG_SO="@TOP_BUILD_DIR@/src/hooks/d2/gss_tsig/libddns_gss_tsig.so"',
],
dependencies: [gtest, crypto],
include_directories: [include_directories('.')] + INCLUDES,
link_with: LIBS_BUILT_SO_FAR,
)
test('ddns-gss-tsig-libloadtests', ddns_gss_tsig_libloadtests, protocol: 'gtest')

View File

@ -0,0 +1,19 @@
// Copyright (C) 2021-2024 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);
}

View File

@ -0,0 +1,136 @@
// Copyright (C) 2021-2024 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 <cryptolink/crypto_rng.h>
#include <gss_tsig_context.h>
#include <gss_tsig_log.h>
#include <managed_key.h>
#include <util/chrono_time_utils.h>
#include <cstring>
#include <iostream>
using namespace isc;
using namespace isc::data;
using namespace isc::dns;
using namespace isc::util;
using namespace std;
namespace isc {
namespace gss_tsig {
string
ManagedKey::statusToText(Status status) {
switch (status) {
case NOT_READY:
return ("not yet ready");
case USABLE:
return ("usable");
case EXPIRED:
return ("expired");
default:
return ("in error");
}
}
string
ManagedKey::genName(const string& suffix) {
uint32_t n;
vector<uint8_t> r = isc::cryptolink::random(sizeof(uint32_t));
memmove(&n, &r[0], sizeof(uint32_t));
ostringstream s;
s << n << "." << suffix;
return (s.str());
}
ManagedKey::ManagedKey(const string& name)
: GssTsigKey(name), parent_id_(""), status_(NOT_READY),
tkey_status_(TKeyExchange::OTHER), tkey_ex_(), mutex_(new mutex()) {
}
void
ManagedKey::operator()(TKeyExchange::Status tkey_status) {
bool success = true;
{
lock_guard<mutex> lock(*mutex_);
setTKeyStatus(tkey_status);
if (tkey_status == TKeyExchange::SUCCESS) {
setStatus(USABLE);
} else {
setStatus(IN_ERROR);
success = false;
}
}
if (success) {
LOG_DEBUG(gss_tsig_logger, log::DBGLVL_TRACE_BASIC,
GSS_TSIG_NEW_KEY_SETUP_SUCCEED)
.arg(getKeyName().toText(true));
} else {
LOG_WARN(gss_tsig_logger, GSS_TSIG_NEW_KEY_SETUP_FAILED)
.arg(getKeyName().toText(true))
.arg(TKeyExchange::statusToText(tkey_status));
}
if (getTKeyExchange() && getTKeyExchange()->getIOService()) {
getTKeyExchange()->getIOService()->post([this]() { getTKeyExchange().reset(); });
}
}
ElementPtr
ManagedKey::toElement() const {
ElementPtr map = Element::createMap();
// Name.
map->set("name", Element::create(getKeyNameStr()));
// Parent.
map->set("server-id", Element::create(getParentID()));
// Status.
map->set("status", Element::create(ManagedKey::statusToText(status_)));
// Per status extra information.
switch (status_) {
case USABLE:
case EXPIRED:
// Security context lifetime.
try {
if (sec_ctx_.get()) {
uint32_t lifetime = sec_ctx_->getLifetime();
map->set("security-context-lifetime",
Element::create(static_cast<long long>(lifetime)));
}
} catch (...) {
// Just ignore errors.
}
break;
case IN_ERROR:
// TKEY status.
map->set("tkey-status",
Element::create(TKeyExchange::statusToText(tkey_status_)));
break;
default:
// TKEY exchange.
map->set("tkey-exchange", Element::create(!!tkey_ex_));
break;
}
// Inception date.
map->set("inception-date", Element::create(clockToText(inception_)));
// Expire date.
map->set("expire-date", Element::create(clockToText(expire_)));
return (map);
}
TSIGContextPtr
ManagedKey::createContext() {
return (GssTsigContextPtr(new GssTsigContext(*this)));
}
} // end of namespace isc::gss_tsig
} // end of namespace isc

View File

@ -0,0 +1,161 @@
// Copyright (C) 2021-2022 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 MANAGED_KEY_H
#define MANAGED_KEY_H
#include <cc/cfg_to_element.h>
#include <gss_tsig_key.h>
#include <tkey_exchange.h>
#include <list>
#include <mutex>
namespace isc {
namespace gss_tsig {
/// @brief Managed GSS-TSIG key.
///
/// This class extends @c GssTsigKey with data which are defined in headers
/// which include the class definition.
class ManagedKey : public GssTsigKey, public TKeyExchange::Callback,
public isc::data::CfgToElement {
public:
/// @brief A key status.
///
/// Standard flow is initialized as not ready, after the setup usable
/// and at expire finishes as expired. When the setup fails it is
/// in error, the TKEY exchange status giving more details on the
/// failure reason.
enum Status {
NOT_READY, ///< Not yet ready (not yet usable).
USABLE, ///< Usable.
EXPIRED, ///< Expired (no longer usable).
IN_ERROR ///< Setup failed.
};
/// @brief Convert a status to its textual form.
static std::string statusToText(Status status);
/// @brief Constructor.
///
/// @param name Key name.
ManagedKey(const std::string& name);
/// @brief Destructor.
virtual ~ManagedKey() = default;
/// @brief Get the key name as a string.
///
/// @return the key name as a string.
std::string getKeyNameStr() const {
return (getKeyName().toText());
}
/// @brief Get the DNS server (parent) ID.
///
/// @return The DNS server (parent) ID.
std::string getParentID() const {
return (parent_id_);
}
/// @brief Set the DNS server (parent) ID.
///
/// @param parent_id DNS server (parent) ID.
void setParentID(const std::string& parent_id) {
parent_id_ = parent_id;
}
/// @brief Get the key status.
///
/// @return The key status.
Status getStatus() const {
return (status_);
}
/// @brief Set the key status.
///
/// @param status The new key status.
void setStatus(Status status) {
status_ = status;
}
/// @brief Get the TKEY exchange status.
///
/// @return The TKEY exchange status.
TKeyExchange::Status getTKeyStatus() const {
return (tkey_status_);
}
/// @brief Set the TKEY exchange status.
///
/// @param tkey_status The new TKEY exchange status.
void setTKeyStatus(TKeyExchange::Status tkey_status) {
tkey_status_ = tkey_status;
}
/// @brief Get the TKEY exchange.
///
/// @return A reference to the TKEY exchange pointer.
TKeyExchangePtr& getTKeyExchange() {
return (tkey_ex_);
}
/// @brief The TKEY exchange completion handler.
///
/// @param tkey_status The completion status.
void operator()(TKeyExchange::Status tkey_status);
/// @brief Create a random name from a suffix.
///
/// @param server The server suffix.
static std::string genName(const std::string& server);
/// @brief Unparse a key object.
///
/// Used to get the full state of a key:
/// - name
/// - status
/// - security-context-lifetime (usable or expired key)
/// - tkey-status (in case of error)
/// - tkey-exchange (not yet ready key)
/// - inception-date
/// - expire-date
///
/// @return a pointer to unparsed key object.
isc::data::ElementPtr toElement() const;
/// @brief Create GssTsigContext context.
///
/// @note overwritten @c isc::d2::D2TsigKey method.
///
/// @return The specific @ref GssTsigContext of the @ref GssTsigKey.
virtual dns::TSIGContextPtr createContext();
private:
/// @brief DNS server (parent) ID.
std::string parent_id_;
/// @brief Key status.
Status status_;
/// @brief TKEY exchange status.
TKeyExchange::Status tkey_status_;
/// @brief TKEY exchange (not null during setup).
TKeyExchangePtr tkey_ex_;
public:
/// @brief Mutex for protecting key state.
boost::scoped_ptr<std::mutex> mutex_;
};
/// @brief Type of pointer to a Managed GSS-TSIG key.
typedef boost::shared_ptr<ManagedKey> ManagedKeyPtr;
} // end of namespace isc::gss_tsig
} // end of namespace isc
#endif // MANAGED_KEY_H

View File

@ -0,0 +1,31 @@
if not krb5.found()
subdir_done()
endif
ddns_gss_tsig_lib = shared_library(
'ddns_gss_tsig',
'gss_tsig_api.cc',
'gss_tsig_callouts.cc',
'gss_tsig_cfg.cc',
'gss_tsig_context.cc',
'gss_tsig_impl.cc',
'gss_tsig_key.cc',
'gss_tsig_log.cc',
'gss_tsig_messages.cc',
'managed_key.cc',
'tkey_exchange.cc',
'version.cc',
dependencies: [krb5, crypto],
include_directories: [include_directories('.')] + INCLUDES,
install: true,
install_dir: 'lib/kea/hooks',
link_with: LIBS_BUILT_SO_FAR,
name_suffix: 'so',
)
ddns_gss_tsig_archive = static_library(
'ddns_gss_tsig',
objects: ddns_gss_tsig_lib.extract_all_objects(recursive: false),
)
subdir('testutils')
subdir('libloadtests')
subdir('tests')

View File

@ -0,0 +1,2 @@
gss_tsig_unittests
nsupdate

View File

@ -0,0 +1,103 @@
SUBDIRS = .
AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
AM_CPPFLAGS += -I$(top_srcdir)/src/bin -I$(top_builddir)/src/bin
AM_CPPFLAGS += -I$(top_builddir)/src/hooks/d2/gss_tsig -I$(top_srcdir)/src/hooks/d2/gss_tsig
AM_CPPFLAGS += -DTEST_DATA_DIR=\"$(abs_srcdir)\"
AM_CPPFLAGS += $(GSSAPI_CFLAGS) $(BOOST_INCLUDES)
AM_CPPFLAGS += $(CRYPTO_CFLAGS) $(CRYPTO_INCLUDES)
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 = doc.txt administrator.ccache testdenied.ccache dns.keytab
CLEANFILES = *.gcno *.gcda
TESTS_ENVIRONMENT = $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
TESTS =
if HAVE_GTEST
TESTS += gss_tsig_unittests
gss_tsig_unittests_SOURCES = run_unittests.cc
gss_tsig_unittests_SOURCES += dns_update_unittests.cc
gss_tsig_unittests_SOURCES += gss_tsig_api_unittests.cc
gss_tsig_unittests_SOURCES += gss_tsig_api_utils.h
gss_tsig_unittests_SOURCES += gss_tsig_callouts_unittests.cc
gss_tsig_unittests_SOURCES += gss_tsig_cfg_unittests.cc
gss_tsig_unittests_SOURCES += gss_tsig_context_unittests.cc
gss_tsig_unittests_SOURCES += gss_tsig_impl_unittests.cc
gss_tsig_unittests_SOURCES += gss_tsig_key_unittests.cc
gss_tsig_unittests_SOURCES += managed_key_unittests.cc
gss_tsig_unittests_SOURCES += tkey_unittests.cc
gss_tsig_unittests_SOURCES += tkey_exchange_unittests.cc
gss_tsig_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) $(LOG4CPLUS_INCLUDES)
gss_tsig_unittests_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) $(GTEST_LDFLAGS)
gss_tsig_unittests_CXXFLAGS = $(AM_CXXFLAGS)
gss_tsig_unittests_LDADD = $(top_builddir)/src/hooks/d2/gss_tsig/testutils/libgsstsigtest.la
gss_tsig_unittests_LDADD += $(top_builddir)/src/hooks/d2/gss_tsig/libgss_tsig.la
gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/d2srv/testutils/libd2srvtest.la
gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/d2srv/libkea-d2srv.la
gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/process/libkea-process.la
gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la
gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/asiodns/libkea-asiodns.la
gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la
gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la
gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/dhcp/testutils/libdhcptest.la
gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la
gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la
gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/database/libkea-database.la
gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/testutils/libkea-testutils.la
gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la
gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/dns/libkea-dns++.la
gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la
gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la
gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/util/libkea-util.la
gss_tsig_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
gss_tsig_unittests_LDADD += $(GSSAPI_LIBS)
gss_tsig_unittests_LDADD += $(LOG4CPLUS_LIBS)
gss_tsig_unittests_LDADD += $(CRYPTO_LIBS)
gss_tsig_unittests_LDADD += $(BOOST_LIBS)
gss_tsig_unittests_LDADD += $(GTEST_LDADD)
nsupdate_SOURCES = nsupdate.cc
nsupdate_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) $(LOG4CPLUS_INCLUDES)
nsupdate_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) $(GTEST_LDFLAGS)
nsupdate_CXXFLAGS = $(AM_CXXFLAGS)
nsupdate_LDADD = $(top_builddir)/src/hooks/d2/gss_tsig/libgss_tsig.la
nsupdate_LDADD += $(top_builddir)/src/lib/d2srv/libkea-d2srv.la
nsupdate_LDADD += $(top_builddir)/src/lib/process/libkea-process.la
nsupdate_LDADD += $(top_builddir)/src/lib/asiodns/libkea-asiodns.la
nsupdate_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la
nsupdate_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la
nsupdate_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
nsupdate_LDADD += $(top_builddir)/src/lib/dns/libkea-dns++.la
nsupdate_LDADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la
nsupdate_LDADD += $(top_builddir)/src/lib/log/libkea-log.la
nsupdate_LDADD += $(top_builddir)/src/lib/util/libkea-util.la
nsupdate_LDADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
nsupdate_LDADD += $(GSSAPI_LIBS) $(LOG4CPLUS_LIBS) $(CRYPTO_LIBS) $(BOOST_LIBS)
noinst_PROGRAMS = $(TESTS) nsupdate
endif
# Heimdal requires restricted permissions on the credential cache files.
check-recursive: pre-check-recursive
pre-check-recursive:
chmod go-rw $(abs_srcdir)/administrator.ccache
chmod go-rw $(abs_srcdir)/testdenied.ccache
.PHONY=pre-check-recursive

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,387 @@
// Copyright (C) 2021-2024 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/interval_timer.h>
#include <asiodns/io_fetch.h>
#include <d2srv/dns_client.h>
#include <d2srv/testutils/stats_test_utils.h>
#include <dns/rcode.h>
#include <gss_tsig_api_utils.h>
#include <managed_key.h>
#include <testutils/gss_tsig_dns_server.h>
#include <util/chrono_time_utils.h>
#include <chrono>
#include <gtest/gtest.h>
namespace {
const char* TEST_ADDRESS = "127.0.0.1";
const uint16_t TEST_PORT = 5376;
const long TEST_TIMEOUT = 5 * 1000; // expressed in milliseconds.
using namespace isc;
using namespace isc::asiolink;
using namespace isc::asiodns;
using namespace isc::d2;
using namespace isc::d2::test;
using namespace isc::dns;
using namespace isc::gss_tsig;
using namespace isc::gss_tsig::test;
using namespace isc::util;
using namespace std;
using namespace std::chrono;
typedef std::function<void()> DNSCallback;
/// @brief Callback class used to handle the TKEY exchange status.
class NSUpdateTestCallback : public TKeyExchange::Callback {
public:
/// @brief Constructor
///
/// @param io_service The IOService which handles IO operations.
/// @param callback The callback used as a continuation after the TKEY
/// exchange is completed.
/// @param status The expected status.
NSUpdateTestCallback(IOServicePtr io_service, DNSCallback& callback,
TKeyExchange::Status status = TKeyExchange::SUCCESS) :
io_service_(io_service), callback_(callback), status_(status) {
}
/// @brief Destructor.
~NSUpdateTestCallback() = default;
/// @brief The callback function which retrieves the TKEY exchange status.
void operator()(TKeyExchange::Status status) override {
if (status != status_) {
ADD_FAILURE() << "key exchange returned " << status
<< ", expected: " << status_;
io_service_->stop();
return;
}
if (status != TKeyExchange::SUCCESS) {
io_service_->stop();
return;
}
if (callback_) {
callback_();
} else {
io_service_->stop();
}
}
/// @brief The IOService which handles IO operations.
IOServicePtr io_service_;
/// @brief The callback called after the GSS-TSIG TKEY exchange has
/// succeeded so that the GSS-TSIG DNS update is performed.
DNSCallback& callback_;
/// @brief The expected TKEY exchange status.
TKeyExchange::Status status_;
};
/// @brief Test fixture for testing the GSS-API GSS-TSIG DNS update with
/// Kerberos 5.
class GssTsigDNSUpdateTest : public GssApiBaseTest, public DNSClient::Callback,
public D2StatTest {
public:
/// @brief Constructor.
GssTsigDNSUpdateTest() : io_service_(new IOService()),
test_timer_(io_service_), response_(),
dns_client_(new DNSClient(response_, this)),
expected_status_(DNSClient::SUCCESS),
success_(false), use_fallback_(false) {
// Set the test timeout to break any running tasks if they hang.
test_timer_.setup(std::bind(&GssTsigDNSUpdateTest::testTimeoutHandler, this),
TEST_TIMEOUT);
}
/// @brief Destructor.
virtual ~GssTsigDNSUpdateTest() {
key_->getTKeyExchange().reset();
test_timer_.cancel();
io_service_->stopAndPoll();
}
/// @brief Perform the DNS update.
void doGssTsigDNSUpdate() {
auto key = key_;
if (use_fallback_) {
key.reset();
}
// Create a request DNS Update message.
D2UpdateMessage message(D2UpdateMessage::OUTBOUND);
ASSERT_NO_THROW(message.setRcode(Rcode(Rcode::NOERROR_CODE)));
ASSERT_NO_THROW(message.setZone(Name("example.com"), RRClass::IN()));
// The socket is now ready to receive the data. Let's post some request
// message then. Set timeout to some reasonable value to make sure that
// there is sufficient amount of time for the test to generate a
// response.
const int timeout = 500;
dns_client_->doUpdate(io_service_, IOAddress(TEST_ADDRESS), TEST_PORT,
message, timeout, key);
}
/// @brief Exchange completion callback
///
/// This callback is called when the exchange with the DNS server is
/// complete or an error occurred. This includes the occurrence of a timeout.
///
/// @param status A status code returned by DNSClient.
virtual void operator()(DNSClient::Status status) override {
io_service_->stop();
EXPECT_EQ(expected_status_, status);
if (expected_status_ == DNSClient::SUCCESS) {
// We should have received a signed response.
ASSERT_TRUE(response_);
EXPECT_EQ(D2UpdateMessage::RESPONSE, response_->getQRFlag());
ASSERT_EQ(1, response_->getRRCount(D2UpdateMessage::SECTION_ZONE));
D2ZonePtr zone = response_->getZone();
ASSERT_TRUE(zone);
EXPECT_EQ("example.com.", zone->getName().toText());
EXPECT_EQ(RRClass::IN().getCode(), zone->getClass().getCode());
// DNS update has been signed on the server side and verified on the
// client side.
success_ = true;
} else if (expected_status_ == DNSClient::INVALID_RESPONSE) {
// We should have received an unsigned response.
ASSERT_TRUE(response_);
EXPECT_EQ(D2UpdateMessage::RESPONSE, response_->getQRFlag());
ASSERT_EQ(1, response_->getRRCount(D2UpdateMessage::SECTION_ZONE));
D2ZonePtr zone = response_->getZone();
ASSERT_FALSE(zone);
}
}
/// @brief Handler invoked when test timeout is hit.
///
/// This callback stops all running (hanging) tasks on IO service.
void testTimeoutHandler() {
io_service_->stop();
FAIL() << "Test timeout hit.";
}
/// @brief The IOService which handles IO operations.
IOServicePtr io_service_;
/// @brief The timeout timer.
asiolink::IntervalTimer test_timer_;
/// @brief DNS client response.
D2UpdateMessagePtr response_;
/// @brief The DNS client performing GSS-TSIG DNS update.
DNSClientPtr dns_client_;
/// @brief The DNS client key.
ManagedKeyPtr key_;
/// @brief The expected status of the DNS client update callback.
DNSClient::Status expected_status_;
/// @brief The flag which indicates that the DNS update has been
/// successfully verified.
bool success_;
/// @brief The flag which indicates if the DNS update should fallback to non
/// GSS-TSIG if the key is removed.
bool use_fallback_;
};
/// @brief Check GSS-TSIG DNS update fails because the DNS server is not
/// responding to DNS updates.
TEST_F(GssTsigDNSUpdateTest, runGssTsigDNSUpdateTimeout) {
expected_status_ = DNSClient::TIMEOUT;
string key_name("1234.sig-blu.example.nil.");
key_.reset(new ManagedKey(key_name));
DnsServerPtr server(new DnsServer("foo", {}, IOAddress(TEST_ADDRESS),
TEST_PORT));
server->setServerPrincipal("DNS/blu.example.nil@EXAMPLE.NIL");
server->setKeyProto(IOFetch::Protocol::UDP);
setKeytab();
setAdministratorCCache();
DNSCallback dns_callback = std::bind(&GssTsigDNSUpdateTest::doGssTsigDNSUpdate, this);
NSUpdateTestCallback callback(io_service_, dns_callback);
system_clock::time_point now = system_clock::now();
key_->setInception(now);
key_->setExpire(now + seconds(server->getKeyLifetime()));
key_->getTKeyExchange().reset(new TKeyExchange(io_service_, server, key_,
&callback));
key_->getTKeyExchange()->doExchange();
// The server will sign the TKEY and stop responding to DNS updates.
DummyDNSServer dns_server(io_service_, true, true, false, false);
dns_server.start();
io_service_->run();
ASSERT_FALSE(success_);
// Check statistics.
StatMap stats_key = {
{ "update-sent", 1},
{ "update-success", 0},
{ "update-timeout", 1},
{ "update-error", 0}
};
checkStats(key_name, stats_key);
StatMap stats_upd = {
{ "update-sent", 1},
{ "update-signed", 1},
{ "update-unsigned", 0},
{ "update-success", 0},
{ "update-timeout", 1},
{ "update-error", 0}
};
checkStats(stats_upd);
}
/// @brief Check GSS-TSIG DNS update fails because the DNS update is not signed.
TEST_F(GssTsigDNSUpdateTest, runGssTsigDNSUpdateFailure) {
expected_status_ = DNSClient::INVALID_RESPONSE;
string key_name("1234.sig-blu.example.nil.");
key_.reset(new ManagedKey(key_name));
DnsServerPtr server(new DnsServer("foo", {}, IOAddress(TEST_ADDRESS),
TEST_PORT));
server->setServerPrincipal("DNS/blu.example.nil@EXAMPLE.NIL");
server->setKeyProto(IOFetch::Protocol::UDP);
setKeytab();
setAdministratorCCache();
DNSCallback dns_callback = std::bind(&GssTsigDNSUpdateTest::doGssTsigDNSUpdate, this);
NSUpdateTestCallback callback(io_service_, dns_callback);
system_clock::time_point now = system_clock::now();
key_->setInception(now);
key_->setExpire(now + seconds(server->getKeyLifetime()));
key_->getTKeyExchange().reset(new TKeyExchange(io_service_, server, key_,
&callback));
key_->getTKeyExchange()->doExchange();
// The server will sign the TKEY, but not sign the DNS update.
DummyDNSServer dns_server(io_service_, true, false);
dns_server.start();
io_service_->run();
ASSERT_FALSE(success_);
// Check statistics.
StatMap stats_key = {
{ "update-sent", 1},
{ "update-success", 0},
{ "update-timeout", 0},
{ "update-error", 1}
};
checkStats(key_name, stats_key);
StatMap stats_upd = {
{ "update-sent", 1},
{ "update-signed", 1},
{ "update-unsigned", 0},
{ "update-success", 0},
{ "update-timeout", 0},
{ "update-error", 1}
};
checkStats(stats_upd);
}
/// @brief Check GSS-TSIG DNS update succeeds.
TEST_F(GssTsigDNSUpdateTest, runGssTsigDNSUpdateSuccess) {
string key_name("1234.sig-blu.example.nil.");
key_.reset(new ManagedKey(key_name));
DnsServerPtr server(new DnsServer("foo", {}, IOAddress(TEST_ADDRESS),
TEST_PORT));
server->setServerPrincipal("DNS/blu.example.nil@EXAMPLE.NIL");
server->setKeyProto(IOFetch::Protocol::UDP);
setKeytab();
setAdministratorCCache();
DNSCallback dns_callback = std::bind(&GssTsigDNSUpdateTest::doGssTsigDNSUpdate, this);
NSUpdateTestCallback callback(io_service_, dns_callback);
system_clock::time_point now = system_clock::now();
key_->setInception(now);
key_->setExpire(now + seconds(server->getKeyLifetime()));
key_->getTKeyExchange().reset(new TKeyExchange(io_service_, server, key_,
&callback));
key_->getTKeyExchange()->doExchange();
// The server will sign both the TKEY and the DNS update.
DummyDNSServer dns_server(io_service_);
dns_server.start();
io_service_->run();
ASSERT_TRUE(success_);
// Check statistics.
StatMap stats_key = {
{ "update-sent", 1},
{ "update-success", 1},
{ "update-timeout", 0},
{ "update-error", 0}
};
checkStats(key_name, stats_key);
StatMap stats_upd = {
{ "update-sent", 1},
{ "update-signed", 1},
{ "update-unsigned", 0},
{ "update-success", 1},
{ "update-timeout", 0},
{ "update-error", 0}
};
checkStats(stats_upd);
}
/// @brief Check GSS-TSIG DNS update succeeds.
TEST_F(GssTsigDNSUpdateTest, runGssTsigDNSUpdateSuccessWithFallback) {
string key_name("1234.sig-blu.example.nil.");
key_.reset(new ManagedKey(key_name));
DnsServerPtr server(new DnsServer("foo", {}, IOAddress(TEST_ADDRESS),
TEST_PORT));
server->setServerPrincipal("DNS/blu.example.nil@EXAMPLE.NIL");
server->setKeyProto(IOFetch::Protocol::UDP);
use_fallback_ = true;
setKeytab();
setAdministratorCCache();
DNSCallback dns_callback = std::bind(&GssTsigDNSUpdateTest::doGssTsigDNSUpdate, this);
NSUpdateTestCallback callback(io_service_, dns_callback);
system_clock::time_point now = system_clock::now();
key_->setInception(now);
key_->setExpire(now + seconds(server->getKeyLifetime()));
key_->getTKeyExchange().reset(new TKeyExchange(io_service_, server, key_,
&callback));
key_->getTKeyExchange()->doExchange();
// The server will sign the TKEY, but not sign the DNS update.
DummyDNSServer dns_server(io_service_, true, false);
dns_server.start();
io_service_->run();
ASSERT_TRUE(success_);
// Check statistics.
StatMap stats_key = {
{ "update-sent", 0},
{ "update-success", 0},
{ "update-timeout", 0},
{ "update-error", 0}
};
checkStats(key_name, stats_key);
StatMap stats_upd = {
{ "update-sent", 1},
{ "update-signed", 0},
{ "update-unsigned", 1},
{ "update-success", 1},
{ "update-timeout", 0},
{ "update-error", 0}
};
checkStats(stats_upd);
}
}

View File

@ -0,0 +1,74 @@
Test tools and files:
- nsupdate: test tool doing the same as BIND 9 nsupdate -g in the tsiggss
system test
- dns.keytab: Kerberos 5 key table file from the BIND 9 tsiggss system test
- administrator.ccache: Kerberos 5 credential cache from the BIND 9 tsiggss
system test for the Administrator@EXAMPLE.NIL principal
- testdenied.ccache: Kerberos 5 credential cache from the BIND 9 tsiggss
system test for the testdenied@EXAMPLE.NIL principal
The BIND 9 tsiggss system test can be found at bin/tests/system/tsiggss
in the BIND 9 sources. There is no script to build them but the
bin/tests/system/nsupdate/krb/setup.sh script builds various Kerberos 5
key table and credential cache files.
The KRB5_KTNAME and KRB5CCNAME environment variables instruct Kerberos 5
support library how/where to find principal properties. To help writing
of new unit tests gss_tsig_api_utils.h defines GssApiBaseTest class
with methods to set and restore these environment variables.
A typical credential cache file content is:
Credentials cache: FILE:bin/tests/system/tsiggss/ns1/administrator.ccache
Principal: administrator@EXAMPLE.NIL
Issued Expires Principal
Nov 30 11:51:16 2010 Apr 6 19:04:36 2036 krbtgt/EXAMPLE.NIL@EXAMPLE.NIL
Nov 30 11:52:12 2010 Apr 6 19:04:36 2036 DNS/blu.example.nil@EXAMPLE.NIL
And the key table is:
Vno Type Principal Date Aliases
1 des-cbc-crc-deprecated DNS/example.nil@EXAMPLE.NIL 2010-11-30
1 des-cbc-crc-deprecated DNS/blu.example.nil@EXAMPLE.NIL 2010-11-30
1 des-cbc-crc-deprecated dns-blu@EXAMPLE.NIL 2010-11-30
1 des-cbc-md5-deprecated DNS/example.nil@EXAMPLE.NIL 2010-11-30
1 des-cbc-md5-deprecated DNS/blu.example.nil@EXAMPLE.NIL 2010-11-30
1 des-cbc-md5-deprecated dns-blu@EXAMPLE.NIL 2010-11-30
1 arcfour-hmac-md5 DNS/example.nil@EXAMPLE.NIL 2010-11-30
1 arcfour-hmac-md5 DNS/blu.example.nil@EXAMPLE.NIL 2010-11-30
1 arcfour-hmac-md5 dns-blu@EXAMPLE.NIL 2010-11-30
1 aes128-cts-hmac-sha1-96 DNS/example.nil@EXAMPLE.NIL 2010-11-30
1 aes128-cts-hmac-sha1-96 DNS/blu.example.nil@EXAMPLE.NIL 2010-11-30
1 aes128-cts-hmac-sha1-96 dns-blu@EXAMPLE.NIL 2010-11-30
1 aes256-cts-hmac-sha1-96 DNS/example.nil@EXAMPLE.NIL 2010-11-30
1 aes256-cts-hmac-sha1-96 DNS/blu.example.nil@EXAMPLE.NIL 2010-11-30
1 aes256-cts-hmac-sha1-96 dns-blu@EXAMPLE.NIL 2010-11-30
nsupdate arguments are optional and are:
-s <addr>[/port]: DNS server address and port (default 10.53.0.1/5300)
-p <princ>: DNS server principal (default 'DNS/blu.example.nil@EXAMPLE.NIL')
-c <cred>: Client credential principal (default none: using default
credential)
-d <key-domain>: Key domain / prefix (default 'sig-blu.example.nil.')
-k <full-key-name>: Key name (default '<random>.sig-blu.example.nil.')
Random is a 32 bit unsigned. Note that a key name can't be reused.
-C <ccache>: Credential cache (default 'FILE:<pwd>/administrator.ccache')
where <pwd> is this directory name. When set to '' the environment
variable is not set by nsupdate.
-n <name>: DNS name to update (default 'testdc1.example.nil.')
-z <zone>: DNS zone to update (default 'example.nil.')
Note the name to update is supposed to be in the zone.
-a <addr4>: IPv4 address to update (default '10.53.0.10')
-t <ttl>: Time To Live of update (default 86400 (one day))
-l <lifetime>: TKEY key lifetime (default 3600 (one hour))
-w <wait>: I/O timeout in ms (default 2000 (2 seconds))
-q <qid>: Query ID base (default 0x1234)
Note the tool managing exchange resets the query id to a pseudo-random value.
-f <flags>: GSS-API flags (default 0x26 ie anti-replay, mutual authentication
and integrity check, but no sequence check)
-u: Use the UDP protocol (default is to use TCP)
-v: verbose flag (default is off)
-h: help (usage and defaults)
All defaults are the same as nsupsate -g in the first tsiggss BIND 9 system
test.

View File

@ -0,0 +1,415 @@
// Copyright (C) 2021-2024 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 <dns/time_utils.h>
#include <gss_tsig_api.h>
#include <gss_tsig_api_utils.h>
#include <testutils/gtest_utils.h>
#include <gtest/gtest.h>
#include <cstdlib>
using namespace std;
using namespace isc;
using namespace isc::gss_tsig;
using namespace isc::gss_tsig::test;
using namespace isc::util;
namespace {
/// @brief Test fixture for testing the GSS-API with Kerberos 5.
class GssApiTest : public GssApiBaseTest {
};
/// @brief Check if the GssApiBuffer constructors build the buffer properly.
TEST_F(GssApiTest, buffer) {
GssApiBufferPtr buf;
EXPECT_NO_THROW(buf.reset(new GssApiBuffer()));
ASSERT_TRUE(buf);
ASSERT_TRUE(buf->getPtr());
EXPECT_EQ(0, buf->getLength());
EXPECT_FALSE(buf->getValue());
EXPECT_TRUE(buf->empty());
const vector<uint8_t>& empty = buf->getContent();
EXPECT_EQ(0, empty.size());
const vector<uint8_t>& test = { 1, 2, 3, 0 };
EXPECT_NO_THROW(buf.reset(new GssApiBuffer(test)));
ASSERT_TRUE(buf);
ASSERT_TRUE(buf->getPtr());
EXPECT_EQ(4, buf->getLength());
EXPECT_TRUE(buf->getValue());
EXPECT_FALSE(buf->empty());
const vector<uint8_t>& content = buf->getContent();
ASSERT_EQ(4, content.size());
EXPECT_EQ(0, memcmp(&test[0], &content[0], 4));
const string& strpp = buf->getString();
string expected("\x1\x2\x3\0");
// Enforce the embedded nul.
expected.resize(4);
EXPECT_EQ(expected, strpp);
EXPECT_EQ(4, strpp.size());
EXPECT_EQ(3, strlen(strpp.c_str()));
EXPECT_NO_THROW(buf.reset(new GssApiBuffer(strpp)));
ASSERT_TRUE(buf);
ASSERT_TRUE(buf->getPtr());
EXPECT_EQ(4, buf->getLength());
EXPECT_TRUE(buf->getValue());
EXPECT_FALSE(buf->empty());
EXPECT_EQ(0, memcmp(&test[0], buf->getValue(), 4));
const string& str = buf->getString(true);
// Trim the embedded nul.
expected.resize(3);
EXPECT_EQ(expected, str);
EXPECT_EQ(3, str.size());
EXPECT_EQ(3, strlen(str.c_str()));
EXPECT_NO_THROW(buf.reset(new GssApiBuffer(str)));
ASSERT_TRUE(buf);
ASSERT_TRUE(buf->getPtr());
EXPECT_EQ(3, buf->getLength());
EXPECT_TRUE(buf->getValue());
EXPECT_FALSE(buf->empty());
EXPECT_EQ(0, memcmp(&test[0], buf->getValue(), 4));
EXPECT_NO_THROW(buf.reset(new GssApiBuffer(4, &test[0])));
ASSERT_TRUE(buf);
ASSERT_TRUE(buf->getPtr());
EXPECT_EQ(4, buf->getLength());
EXPECT_TRUE(buf->getValue());
EXPECT_FALSE(buf->empty());
EXPECT_EQ(0, memcmp(&test[0], buf->getValue(), 4));
}
/// @brief Verify the GSS-API error message display.
TEST_F(GssApiTest, errorMessage) {
string msg = gssApiErrMsg(GSS_S_COMPLETE, GSS_S_COMPLETE);
string expected = "GSSAPI error: Major = '";
#ifndef WITH_HEIMDAL
expected += "The routine completed successfully' (0).";
#else
expected += " Function completed successfully' (0).";
#endif
EXPECT_EQ(expected, msg);
msg = gssApiErrMsg(GSS_S_BAD_NAME, GSS_S_COMPLETE);
expected = "GSSAPI error: Major = '";
#ifndef WITH_HEIMDAL
expected += "An invalid name was supplied' (131072).";
#else
expected += " An invalid name was supplied' (131072).";
#endif
EXPECT_EQ(expected, msg);
}
/// @brief Check the GSS-API name.
TEST_F(GssApiTest, name) {
GssApiNamePtr name;
EXPECT_NO_THROW(name.reset(new GssApiName()));
ASSERT_TRUE(name);
EXPECT_FALSE(name->get());
EXPECT_EQ(0, name->getLastError());
string expected = "gss_display_name failed with GSSAPI error: Major = '";
#ifndef WITH_HEIMDAL
expected += "A required input parameter could not be read' (16908288).";
OM_uint32 expected_major = 0x1020000;
#else
expected += " An invalid name was supplied' (131072).";
OM_uint32 expected_major = 0x20000;
#endif
EXPECT_THROW_MSG(name->toString(), GssApiError, expected);
EXPECT_EQ(expected_major, name->getLastError());
string principal = "DNS/server.example.org@EXAMPLE.NET";
EXPECT_NO_THROW(name.reset(new GssApiName(principal)));
ASSERT_TRUE(name);
ASSERT_TRUE(name->get());
string principal2 = "DNS/server2.example.org@EXAMPLE.NET";
GssApiNamePtr name2;
EXPECT_NO_THROW(name2.reset(new GssApiName(principal2)));
ASSERT_TRUE(name2);
ASSERT_TRUE(name2->get());
EXPECT_TRUE(name->compare(*name));
EXPECT_TRUE(name2->compare(*name2));
EXPECT_FALSE(name2->compare(*name));
EXPECT_FALSE(name->compare(*name2));
string text;
EXPECT_NO_THROW(text = name->toString());
expected = "DNS/server.example.org@EXAMPLE.NET";
EXPECT_EQ(expected, text);
}
/// @brief Check the GSS-API OID.
TEST_F(GssApiTest, oid) {
GssApiOidPtr oid;
EXPECT_NO_THROW(oid.reset(new GssApiOid()));
ASSERT_TRUE(oid);
string expected = "gss_oid_to_str failed with ";
expected += "GSSAPI error: Major = '";
#ifndef WITH_HEIMDAL
expected += "A required input parameter could not be read' (16777216).";
#else
expected += " Miscellaneous failure (see text)' (851968), Minor = '";
expected += "unknown mech-code 1859794437 for mech unknown' (1859794437).";
#endif
EXPECT_THROW_MSG(oid->toString(), GssApiError, expected);
vector<uint8_t> bin =
{ 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x12, 0x01, 0x02, 0x02 };
EXPECT_NO_THROW(oid.reset(new GssApiOid(bin)));
ASSERT_TRUE(oid);
string txt;
EXPECT_NO_THROW(txt = oid->toString());
#ifndef WITH_HEIMDAL
EXPECT_EQ("{ 1 2 840 113554 1 2 2 }", txt);
#else
EXPECT_EQ("1 2 840 113554 1 2 2", txt);
#endif
#ifdef HAVE_GSS_STR_TO_OID
EXPECT_NO_THROW(oid.reset(new GssApiOid("{ 1 2 840 113554 1 2 2 }")));
ASSERT_TRUE(oid);
#endif
}
/// @brief Check the GSS-API credential default.
/// The spec says the default credential makes sense only on the accept side
/// and Heimdal follows this a bit too much...
TEST_F(GssApiTest, credDefault) {
setAdministratorCCache();
GssApiCredPtr cred;
EXPECT_NO_THROW(cred.reset(new GssApiCred()));
ASSERT_TRUE(cred);
EXPECT_FALSE(cred->get());
EXPECT_EQ(0, cred->getLastError());
GssApiName name;
gss_cred_usage_t usage = 0;
OM_uint32 lifetime = 0;
EXPECT_NO_THROW(cred->inquire(name, usage, lifetime));
EXPECT_TRUE(name.get());
// lifetime == 0 means expired.
EXPECT_NE(0, lifetime);
const uint64_t now = static_cast<uint64_t>(time(0));
// krbtgt/EXAMPLE.NIL@EXAMPLE.NIL cached credential expires at
// Apr 6 19:04:36 2036.
const uint64_t expire = timeFromText64("20260406190436");
EXPECT_LE(expire, now + lifetime);
#ifndef WITH_HEIMDAL
string admin;
EXPECT_NO_THROW(admin = name.toString());
EXPECT_EQ("administrator@EXAMPLE.NIL", admin);
EXPECT_EQ(GSS_C_INITIATE, usage);
#endif
}
/// @brief Check the GSS-API credential for an explicit principal.
TEST_F(GssApiTest, credExplicit) {
setKeytab();
GssApiCredPtr cred;
GssApiName name("DNS/blu.example.nil@EXAMPLE.NIL");
OM_uint32 lifetime = 0;
EXPECT_NO_THROW(cred.reset(new GssApiCred(name, GSS_C_ACCEPT,
lifetime)));
EXPECT_NE(0, lifetime);
ASSERT_TRUE(cred);
EXPECT_TRUE(cred->get());
EXPECT_EQ(0, cred->getLastError());
GssApiNamePtr namep(new GssApiName());
gss_cred_usage_t usage = 0;
lifetime = 0;
EXPECT_NO_THROW(cred->inquire(*namep, usage, lifetime));
EXPECT_TRUE(namep->get());
EXPECT_TRUE(namep->compare(name));
EXPECT_EQ(GSS_C_ACCEPT, usage);
// lifetime == 0 means expired.
EXPECT_NE(0, lifetime);
const uint64_t now = static_cast<uint64_t>(time(0));
// DNS/blu.example.nil@EXAMPLE.NIL cached credential expires at
// Apr 6 19:04:36 2036.
const uint64_t expire = timeFromText64("20260406190436");
EXPECT_LE(expire, now + lifetime);
}
/// @brief Check the GSS-API credential for a principal in another realm
/// (here we use no realm as EXAMPLE.NIL is very unlikely the local default).
TEST_F(GssApiTest, credNoRealm) {
setKeytab();
GssApiCredPtr cred;
GssApiName name("DNS/blu.example.nil");
OM_uint32 lifetime = 0;
// Various error messages explaining a reason to fail to acquire the
// the credential.
EXPECT_THROW(cred.reset(new GssApiCred(name, GSS_C_ACCEPT, lifetime)),
GssApiError);
}
/// @brief Check the GSS-API credential for a principal with another service.
TEST_F(GssApiTest, credBadService) {
setKeytab();
GssApiCredPtr cred;
GssApiName name("Foo/blu.example.nil@EXAMPLE.NIL");
OM_uint32 lifetime = 0;
// Various error messages explaining a reason to fail to acquire the
// the credential.
EXPECT_THROW(cred.reset(new GssApiCred(name, GSS_C_ACCEPT, lifetime)),
GssApiError);
}
/// @brief Check the GSS-API credential for another principal.
TEST_F(GssApiTest, credBadName) {
// Use the other credential cache.
setTestdeniedCCache();
GssApiCredPtr cred;
GssApiName name("administrator@EXAMPLE.NIL");
OM_uint32 lifetime = 0;
// Various error messages explaining a reason to fail to acquire the
// the credential.
EXPECT_THROW(cred.reset(new GssApiCred(name, GSS_C_INITIATE, lifetime)),
GssApiError);
}
/// @brief Check GSS-API exchange.
TEST_F(GssApiTest, exchange) {
setKeytab();
setAdministratorCCache();
// Server.
GssApiSecCtx srv_ctx(GSS_C_NO_CONTEXT);
GssApiCredPtr srv_cred;
GssApiName srv_name("DNS/blu.example.nil@EXAMPLE.NIL");
OM_uint32 srv_lifetime = 0;
EXPECT_NO_THROW(srv_cred.reset(new GssApiCred(srv_name, GSS_C_ACCEPT,
srv_lifetime)));
ASSERT_TRUE(srv_cred);
ASSERT_TRUE(srv_cred->get());
// Client.
GssApiSecCtx clnt_ctx(GSS_C_NO_CONTEXT);
GssApiCredPtr clnt_cred;
GssApiName clnt_name;
OM_uint32 flags = GSS_C_REPLAY_FLAG | GSS_C_MUTUAL_FLAG | GSS_C_INTEG_FLAG;
/// @todo: the standard requires this sequence flag but BIND 9 code
/// does not set it because it disturbs (disturbed) Windows DNS servers.
/// flags |= GSS_C_SEQUENCE_FLAG;
OM_uint32 clnt_lifetime = 0;
// Exchange loop.
size_t loop = 0;
bool clnt_ret = false;
bool srv_ret = false;
GssApiBufferPtr clnt_to_srv(new GssApiBuffer());
GssApiBufferPtr srv_to_clnt(new GssApiBuffer());
while (!clnt_ret) {
++loop;
clnt_to_srv.reset(new GssApiBuffer());
clnt_ret = clnt_ctx.init(clnt_cred, srv_name, flags, *srv_to_clnt,
*clnt_to_srv, clnt_lifetime);
if (!clnt_to_srv->empty()) {
srv_to_clnt.reset(new GssApiBuffer());
srv_ret = srv_ctx.accept(*srv_cred, *clnt_to_srv, clnt_name,
*srv_to_clnt);
if (srv_to_clnt->empty()) {
break;
}
}
}
EXPECT_EQ(2, loop);
ASSERT_TRUE(clnt_ret);
ASSERT_TRUE(clnt_ctx.get());
EXPECT_EQ(0, clnt_ctx.getLastError());
// lifetime == 0 means expired.
EXPECT_NE(0, clnt_lifetime);
uint64_t now = static_cast<uint64_t>(time(0));
// Cached credentials expire at Apr 6 19:04:36 2036.
const uint64_t expire = timeFromText64("20260406190436");
EXPECT_LE(expire, now + clnt_lifetime);
ASSERT_TRUE(srv_ret);
ASSERT_TRUE(srv_ctx.get());
EXPECT_EQ(0, srv_ctx.getLastError());
// Inquire the client state.
GssApiName source;
GssApiName target;
clnt_lifetime = 0;
OM_uint32 got_flags = 0;
bool local = false;
bool established = false;
EXPECT_NO_THROW(clnt_ctx.inquire(source, target, clnt_lifetime,
got_flags, local, established));
ASSERT_TRUE(source.get());
string src_txt;
EXPECT_NO_THROW(src_txt = source.toString());
EXPECT_EQ("administrator@EXAMPLE.NIL", src_txt);
ASSERT_TRUE(target.get());
string tgt_txt;
EXPECT_NO_THROW(tgt_txt = target.toString());
EXPECT_EQ("DNS/blu.example.nil@EXAMPLE.NIL", tgt_txt);
// lifetime == 0 means expired.
EXPECT_NE(0, clnt_lifetime);
now = static_cast<uint64_t>(time(0));
EXPECT_LE(expire, now + clnt_lifetime);
EXPECT_EQ(flags, (got_flags & flags));
EXPECT_TRUE(local);
EXPECT_TRUE(established);
// Inquire the server state.
GssApiName source2;
GssApiName target2;
srv_lifetime = 0;
got_flags = 0;
established = false;
EXPECT_NO_THROW(srv_ctx.inquire(source2, target2, srv_lifetime,
got_flags, local, established));
ASSERT_TRUE(source2.get());
src_txt.clear();
EXPECT_NO_THROW(src_txt = source2.toString());
EXPECT_EQ("administrator@EXAMPLE.NIL", src_txt);
ASSERT_TRUE(target2.get());
tgt_txt.clear();
EXPECT_NO_THROW(tgt_txt = target2.toString());
EXPECT_EQ("DNS/blu.example.nil@EXAMPLE.NIL", tgt_txt);
// lifetime == 0 means expired.
EXPECT_NE(0, srv_lifetime);
now = static_cast<uint64_t>(time(0));
EXPECT_LE(expire, now + srv_lifetime);
// Server side shall expire after the client (experiments showed 300s).
EXPECT_LE(clnt_lifetime, srv_lifetime);
EXPECT_EQ(flags, (got_flags & flags));
EXPECT_FALSE(local);
EXPECT_TRUE(established);
// Try sign/verify from client to server.
GssApiBufferPtr tbs;
string tbs_str = "Hi There";
tbs.reset(new GssApiBuffer(tbs_str.size() + 1, tbs_str.c_str()));
GssApiBuffer sign;
EXPECT_NO_THROW(clnt_ctx.sign(*tbs, sign));
ASSERT_FALSE(sign.empty());
EXPECT_NO_THROW(srv_ctx.verify(*tbs, sign));
// Try sign/verify from server to client.
GssApiBufferPtr tbs2;
string tbs2_str = "what do ya want for nothing?";
tbs2.reset(new GssApiBuffer(tbs2_str.size() + 1, tbs2_str.c_str()));
GssApiBuffer sign2;
EXPECT_NO_THROW(srv_ctx.sign(*tbs2, sign2));
ASSERT_FALSE(sign2.empty());
EXPECT_NO_THROW(clnt_ctx.verify(*tbs2, sign2));
}
}

View File

@ -0,0 +1,86 @@
// Copyright (C) 2021-2022 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 GSS_TSIG_API_UTILS_H
#define GSS_TSIG_API_UTILS_H
#include <gtest/gtest.h>
#include <cstdlib>
namespace isc {
namespace gss_tsig {
namespace test {
/// @brief Test fixture for testing the GSS-API with Kerberos 5.
class GssApiBaseTest : public ::testing::Test {
public:
/// @brief Constructor.
GssApiBaseTest() : has_tkname(false), tkname(""),
has_ccname(false), ccname("") {
char* tkname_env = std::getenv("KRB5_KTNAME");
if (tkname_env) {
has_tkname = true;
tkname = std::string(tkname_env);
}
char* ccname_env = std::getenv("KRB5CCNAME");
if (ccname_env) {
has_ccname = true;
ccname = std::string(ccname_env);
}
}
/// @brief Destructor.
~GssApiBaseTest() {
if (has_tkname) {
setenv("KRB5_KTNAME", tkname.c_str(), 1);
} else {
unsetenv("KRB5_KTNAME");
}
if (has_ccname) {
setenv("KRB5CCNAME", ccname.c_str(), 1);
} else {
unsetenv("KRB5CCNAME");
}
}
/// @brief Set the keytab.
void setKeytab() {
std::string keytab = std::string(TEST_DATA_DIR) + "/dns.keytab";
setenv("KRB5_KTNAME", keytab.c_str(), 1);
}
/// @brief Set the administrator credential cache.
void setAdministratorCCache() {
std::string ccache = "FILE:";
ccache += std::string(TEST_DATA_DIR) + "/administrator.ccache";
setenv("KRB5CCNAME", ccache.c_str(), 1);
}
/// @brief Set the testdenied credential cache.
void setTestdeniedCCache() {
std::string ccache = "FILE:";
ccache += std::string(TEST_DATA_DIR) + "/testdenied.ccache";
setenv("KRB5CCNAME", ccache.c_str(), 1);
}
/// @brief KRB5_KTNAME environment variable existed before.
bool has_tkname;
/// @brief Previous value of KRB5_KTNAME environment variable.
std::string tkname;
/// @brief KRB5CCNAME environment variable existed before.
bool has_ccname;
/// @brief Previous value of KRB5CCNAME environment variable.
std::string ccname;
};
} // end of isc::gss_tsig::test namespace
} // end of isc::gss_tsig namespace
} // end of isc namespace
#endif // GSS_TSIG_API_UTILS_H

View File

@ -0,0 +1,259 @@
// Copyright (C) 2017-2024 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 control command legal file entry
/// generation and callout: command_processed.
/// These tests assume the legal log library is linked in, not loaded.
/// This allows a great deal more flexibility in testing, such as overriding
/// and accessing the BackendStore::instance().
/// The load and unload callouts are exercised in ../libloadtests, which
/// actually uses the HooksManager to load and unload the library.
#include <config.h>
#include <asiolink/io_service.h>
#include <d2srv/d2_config.h>
#include <gss_tsig_api_utils.h>
#include <gss_tsig_impl.h>
#include <hooks/callout_manager.h>
#include <hooks/hooks.h>
#include <testutils/gss_tsig_dns_server.h>
#include <testutils/gtest_utils.h>
#include <gtest/gtest.h>
using namespace isc;
using namespace isc::asiolink;
using namespace isc::data;
using namespace isc::d2;
using namespace isc::gss_tsig;
using namespace isc::gss_tsig::test;
using namespace isc::hooks;
using namespace std;
extern "C" {
extern int select_key(CalloutHandle& handle);
}
namespace isc {
namespace gss_tsig {
extern GssTsigImplPtr impl;
}
}
namespace {
const long TEST_TIMEOUT = 5 * 100; // expressed in milliseconds.
/// @brief Test fixture for exercising flex-id library callouts
/// It fetches the CalloutManager and prepares stub packets that can be used in
/// tests.
class CalloutTest : public GssApiBaseTest {
public:
/// @brief Constructor.
CalloutTest() : io_service_(new IOService()), test_timer_(io_service_),
co_manager_(new CalloutManager(1)), cfg_mgr_(new D2CfgMgr()) {
impl.reset(new GssTsigImpl());
string config = "{\n"
"\"ip-address\": \"127.0.0.1\",\n"
"\"port\": 53001,\n"
"\"dns-server-timeout\" : 1000,\n"
"\"forward-ddns\": {\n"
" \"ddns-domains\": [\n"
" {\n"
" \"name\": \"secure.example.org.\",\n"
" \"dns-servers\": [\n"
" {\n"
" \"ip-address\": \"127.0.0.1\",\n"
" \"port\": 5376\n"
" }\n"
" ]\n"
" }\n"
" ]\n"
"},\n"
"\"reverse-ddns\": {\n"
" \"ddns-domains\": [\n"
" {\n"
" \"name\": \"0.0.127.in-addr.arpa.\",\n"
" \"dns-servers\": [\n"
" {\n"
" \"ip-address\": \"127.0.0.1\",\n"
" \"port\": 5376\n"
" }\n"
" ]\n"
" }\n"
" ]\n"
"}\n"
"}\n";
ConstElementPtr config_set = Element::fromJSON(config);
cfg_mgr_->simpleParseConfig(config_set);
string hook_config = "{\n"
"\"server-principal\": \"DNS/blu.example.nil@EXAMPLE.NIL\",\n"
"\"client-principal\": \"administrator@EXAMPLE.NIL\",\n"
"\"tkey-lifetime\": 7200,\n"
"\"tkey-protocol\": \"UDP\",\n"
"\"servers\": [\n"
" {\n"
" \"domain-names\": [ ],\n"
" \"id\": \"foo\",\n"
" \"ip-address\": \"127.0.0.1\",\n"
" \"port\": 5376,\n"
" \"tkey-lifetime\": 86400,\n"
" \"tkey-protocol\": \"UDP\"\n"
" }\n"
"]\n"
"}\n";
setKeytab();
setAdministratorCCache();
ConstElementPtr hook_config_set = Element::fromJSON(hook_config);
impl->configure(hook_config_set);
D2CfgContextPtr d2_config(cfg_mgr_->getD2CfgContext());
impl->setIOService(io_service_);
impl->finishConfigure(d2_config);
EXPECT_NO_THROW(legacy_key_.reset(new TSIGKeyInfo("test_key",
TSIGKeyInfo::HMAC_MD5_STR,
"GWG/Xfbju4O2iXGqkSu4PQ==")));
// Set the test timeout to break any running tasks if they hang.
test_timer_.setup(std::bind(&CalloutTest::testTimeoutHandler, this),
TEST_TIMEOUT);
impl->start();
}
/// @brief Destructor.
~CalloutTest() {
impl.reset();
test_timer_.cancel();
io_service_->stopAndPoll();
}
/// @brief Fetches the callout manager instance.
boost::shared_ptr<CalloutManager>getCalloutManager() {
return (co_manager_);
}
/// @brief Handler invoked when test timeout is hit
///
/// This callback stops all running (hanging) tasks on IO service.
void testTimeoutHandler() {
io_service_->stop();
}
/// @brief The IOService which handles IO operations.
IOServicePtr io_service_;
/// @brief The timeout timer.
asiolink::IntervalTimer test_timer_;
/// @brief Callout manager accessed by this CalloutHandle.
boost::shared_ptr<CalloutManager> co_manager_;
/// @brief Config manager used to store DNS servers.
boost::shared_ptr<D2CfgMgr> cfg_mgr_;
/// @brief The TSIG Key (non GSS-TSIG key).
TSIGKeyInfoPtr legacy_key_;
/// @brief Dummy DNS server.
boost::shared_ptr<DummyDNSServer> dns_server_;
};
TEST_F(CalloutTest, select_keyNoServerArgument) {
CalloutHandle handle(getCalloutManager());
ASSERT_THROW_MSG(select_key(handle), NoSuchArgument,
"unable to find argument with name current_server");
}
TEST_F(CalloutTest, select_keyNoTSIGKeyArgument) {
CalloutHandle handle(getCalloutManager());
handle.setArgument("current_server", DnsServerInfoPtr());
ASSERT_THROW_MSG(select_key(handle), NoSuchArgument,
"unable to find argument with name tsig_key");
}
TEST_F(CalloutTest, select_keyNoGSSWithNoFallback) {
CalloutHandle handle(getCalloutManager());
DnsServerPtr server = impl->getServer("foo");
ASSERT_TRUE(server);
ASSERT_TRUE(server->getServerInfos().size());
DnsServerInfoPtr info = server->getServerInfos()[0];
handle.setArgument("current_server", info);
handle.setArgument("tsig_key", legacy_key_->getTSIGKey());
int ret = select_key(handle);
ASSERT_EQ(ret, 0);
// If no GSS-TSIG TKEY is available, the non GSS-TSIG key is not used and
// the server is skipped by updating the callout status.
ASSERT_EQ(handle.getStatus(), CalloutHandle::NEXT_STEP_SKIP);
D2TsigKeyPtr key;
handle.getArgument("tsig_key", key);
ASSERT_EQ(key, legacy_key_->getTSIGKey());
}
TEST_F(CalloutTest, select_keyNoGSSWithFallback) {
CalloutHandle handle(getCalloutManager());
DnsServerPtr server = impl->getServer("foo");
ASSERT_TRUE(server);
ASSERT_TRUE(server->getServerInfos().size());
server->setFallback(true);
DnsServerInfoPtr info = server->getServerInfos()[0];
handle.setArgument("current_server", info);
handle.setArgument("tsig_key", legacy_key_->getTSIGKey());
int ret = select_key(handle);
ASSERT_EQ(ret, 0);
// If no GSS-TSIG TKEY is available, the non GSS-TSIG key is used.
ASSERT_EQ(handle.getStatus(), CalloutHandle::NEXT_STEP_CONTINUE);
D2TsigKeyPtr key;
handle.getArgument("tsig_key", key);
ASSERT_EQ(key, legacy_key_->getTSIGKey());
}
TEST_F(CalloutTest, select_keyGSSWithNoFallback) {
CalloutHandle handle(getCalloutManager());
dns_server_.reset(new DummyDNSServer(io_service_, true, true, false, true, false));
dns_server_->start();
io_service_->run();
DnsServerPtr server = impl->getServer("foo");
ASSERT_TRUE(server);
ASSERT_TRUE(server->getServerInfos().size());
DnsServerInfoPtr info = server->getServerInfos()[0];
handle.setArgument("current_server", info);
handle.setArgument("tsig_key", legacy_key_->getTSIGKey());
int ret = select_key(handle);
ASSERT_EQ(ret, 0);
// If GSS-TSIG TKEY is available it is used instead on the non GSS-TSIG key.
ASSERT_EQ(handle.getStatus(), CalloutHandle::NEXT_STEP_CONTINUE);
D2TsigKeyPtr key;
handle.getArgument("tsig_key", key);
ASSERT_TRUE(key);
ASSERT_NE(key, legacy_key_->getTSIGKey());
ManagedKeyPtr mkey = boost::dynamic_pointer_cast<ManagedKey>(key);
ASSERT_TRUE(mkey);
}
TEST_F(CalloutTest, select_keyGSSWithFallback) {
CalloutHandle handle(getCalloutManager());
dns_server_.reset(new DummyDNSServer(io_service_, true, true, false, true, false));
dns_server_->start();
io_service_->run();
DnsServerPtr server = impl->getServer("foo");
ASSERT_TRUE(server);
ASSERT_TRUE(server->getServerInfos().size());
server->setFallback(true);
DnsServerInfoPtr info = server->getServerInfos()[0];
handle.setArgument("current_server", info);
handle.setArgument("tsig_key", legacy_key_->getTSIGKey());
int ret = select_key(handle);
ASSERT_EQ(ret, 0);
// If GSS-TSIG TKEY is available it is used instead on the non GSS-TSIG key.
ASSERT_EQ(handle.getStatus(), CalloutHandle::NEXT_STEP_CONTINUE);
D2TsigKeyPtr key;
handle.getArgument("tsig_key", key);
ASSERT_TRUE(key);
ASSERT_NE(key, legacy_key_->getTSIGKey());
ManagedKeyPtr mkey = boost::dynamic_pointer_cast<ManagedKey>(key);
ASSERT_TRUE(mkey);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,651 @@
// Copyright (C) 2021-2024 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 <dns/name.h>
#include <dns/message.h>
#include <dns/messagerenderer.h>
#include <dns/opcode.h>
#include <gss_tsig_context.h>
#include <gss_tsig_api_utils.h>
#include <testutils/gtest_utils.h>
#include <gtest/gtest.h>
using namespace std;
using namespace isc;
using namespace isc::cryptolink;
using namespace isc::dns;
using namespace isc::dns::rdata::any;
using namespace isc::gss_tsig;
using namespace isc::gss_tsig::test;
using namespace isc::util;
/// @todo this is defined as class static constants, but some compilers
/// seemingly cannot find the symbol when used in the EXPECT_xxx macros.
const uint16_t TSIGContext::DEFAULT_FUDGE;
namespace {
/// @brief Test fixture for testing the GSS-TSIG key.
class GssTsigContextTest : public GssApiBaseTest {
public:
/// @brief Constructor.
GssTsigContextTest() : GssApiBaseTest(), message_(Message::RENDER) {
}
/// @brief Destructor.
~GssTsigContextTest() {
}
/// @brief The message used for signing.
Message message_;
};
/// @brief Check the constructor builds what is expected.
TEST_F(GssTsigContextTest, basic) {
GssTsigKeyPtr key;
string name = "1234.sig-foo.example.com.";
ASSERT_NO_THROW(key.reset(new GssTsigKey(name)));
ASSERT_TRUE(key);
GssTsigContextPtr ctx;
ASSERT_NO_THROW(ctx.reset(new GssTsigContext(*key)));
ASSERT_TRUE(ctx);
// Verify context.
EXPECT_EQ(TSIGContext::INIT, ctx->getState());
EXPECT_EQ(TSIGError::NOERROR(), ctx->getError());
// Length is 26 + 26 + 10 + 128 + 0 = 190.
EXPECT_EQ(190, ctx->getTSIGLength());
}
/// @brief Check sign direct errors.
TEST_F(GssTsigContextTest, signError) {
GssTsigKeyPtr key;
string name = "1234.sig-foo.example.com.";
ASSERT_NO_THROW(key.reset(new GssTsigKey(name)));
ASSERT_TRUE(key);
GssTsigContextPtr ctx;
ASSERT_NO_THROW(ctx.reset(new GssTsigContext(*key)));
ASSERT_TRUE(ctx);
// VERIFIED_RESPONSE state is not allowed.
ctx->setState(TSIGContext::VERIFIED_RESPONSE);
EXPECT_EQ(TSIGContext::VERIFIED_RESPONSE, ctx->getState());
EXPECT_THROW_MSG(ctx->sign(0x1234, 0, 0), TSIGContextError,
"TSIG sign attempt after verifying a response");
// Data must not be empty.
ctx->setState(TSIGContext::INIT);
EXPECT_THROW_MSG(ctx->sign(0x1234, 0, 100), InvalidParameter,
"TSIG sign error: empty data is given");
EXPECT_THROW_MSG(ctx->sign(0x1234, &name[0], 0), InvalidParameter,
"TSIG sign error: empty data is given");
}
/// @brief Check verify direct errors.
TEST_F(GssTsigContextTest, verifyError) {
GssTsigKeyPtr key;
string name = "1234.sig-foo.example.com.";
ASSERT_NO_THROW(key.reset(new GssTsigKey(name)));
ASSERT_TRUE(key);
GssTsigContextPtr ctx;
ASSERT_NO_THROW(ctx.reset(new GssTsigContext(*key)));
ASSERT_TRUE(ctx);
// SENT_RESPONSE state is not allowed.
ctx->setState(TSIGContext::SENT_RESPONSE);
EXPECT_EQ(TSIGContext::SENT_RESPONSE, ctx->getState());
EXPECT_THROW_MSG(ctx->verify(0, 0, 0), TSIGContextError,
"TSIG verify attempt after sending a response");
}
/// @brief Check lastHadSignature requires some history.
TEST_F(GssTsigContextTest, lastHadSignature) {
GssTsigKeyPtr key;
string name = "1234.sig-foo.example.com.";
ASSERT_NO_THROW(key.reset(new GssTsigKey(name)));
ASSERT_TRUE(key);
GssTsigContextPtr ctx;
ASSERT_NO_THROW(ctx.reset(new GssTsigContext(*key)));
ASSERT_TRUE(ctx);
EXPECT_THROW_MSG(ctx->lastHadSignature(), TSIGContextError,
"No message was verified yet");
}
/// @brief Check unsigned response.
TEST_F(GssTsigContextTest, unsigned) {
GssTsigKeyPtr key;
string name = "1234.sig-foo.example.com.";
ASSERT_NO_THROW(key.reset(new GssTsigKey(name)));
ASSERT_TRUE(key);
GssTsigContextPtr ctx;
ASSERT_NO_THROW(ctx.reset(new GssTsigContext(*key)));
ASSERT_TRUE(ctx);
// BAD_SIG gives an unsigned response.
ctx->setError(TSIGError::BAD_SIG());
ctx->setState(TSIGContext::RECEIVED_REQUEST);
EXPECT_EQ(TSIGError::BAD_SIG(), ctx->getError());
ConstTSIGRecordPtr tsig;
const string& dummy = "dummy";
uint64_t before = static_cast<uint64_t>(time(0));
ASSERT_NO_THROW(tsig = ctx->sign(0x1234, dummy.c_str(), dummy.size()));
uint64_t after = static_cast<uint64_t>(time(0));
ASSERT_TRUE(tsig);
EXPECT_EQ(Name(name), tsig->getName());
EXPECT_EQ(RRClass::ANY(), tsig->getClass());
EXPECT_EQ(0, tsig->getTTL().getValue());
// Length is 26 + 26 + 10 = 62.
EXPECT_EQ(62, tsig->getLength());
const TSIG& rdata = tsig->getRdata();
EXPECT_EQ(Name("gss-tsig."), rdata.getAlgorithm());
EXPECT_LE(before, rdata.getTimeSigned());
EXPECT_GE(after, rdata.getTimeSigned());
EXPECT_EQ(TSIGContext::DEFAULT_FUDGE, rdata.getFudge());
EXPECT_EQ(0, rdata.getMACSize());
EXPECT_FALSE(rdata.getMAC());
EXPECT_EQ(0x1234, rdata.getOriginalID());
EXPECT_EQ(TSIGError::BAD_SIG_CODE, rdata.getError());
EXPECT_EQ(0, rdata.getOtherLen());
EXPECT_FALSE(rdata.getOtherData());
EXPECT_EQ(TSIGContext::SENT_RESPONSE, ctx->getState());
EXPECT_EQ(TSIGError::BAD_SIG(), ctx->getError());
// Reset the context.
ASSERT_NO_THROW(ctx.reset(new GssTsigContext(*key)));
ASSERT_TRUE(ctx);
// BAD_KEY also gives an unsigned response.
ctx->setError(TSIGError::BAD_KEY());
ctx->setState(TSIGContext::RECEIVED_REQUEST);
EXPECT_EQ(TSIGError::BAD_KEY(), ctx->getError());
before = static_cast<uint64_t>(time(0));
ASSERT_NO_THROW(tsig = ctx->sign(0x1234, dummy.c_str(), dummy.size()));
after = static_cast<uint64_t>(time(0));
ASSERT_TRUE(tsig);
EXPECT_EQ(Name(name), tsig->getName());
EXPECT_EQ(RRClass::ANY(), tsig->getClass());
EXPECT_EQ(0, tsig->getTTL().getValue());
// Length is 26 + 26 + 10 = 62.
EXPECT_EQ(62, tsig->getLength());
const TSIG& rdata2 = tsig->getRdata();
EXPECT_EQ(Name("gss-tsig."), rdata2.getAlgorithm());
EXPECT_LE(before, rdata2.getTimeSigned());
EXPECT_GE(after, rdata2.getTimeSigned());
EXPECT_EQ(TSIGContext::DEFAULT_FUDGE, rdata2.getFudge());
EXPECT_EQ(0, rdata2.getMACSize());
EXPECT_FALSE(rdata2.getMAC());
EXPECT_EQ(0x1234, rdata2.getOriginalID());
EXPECT_EQ(TSIGError::BAD_KEY_CODE, rdata2.getError());
EXPECT_EQ(0, rdata2.getOtherLen());
EXPECT_FALSE(rdata2.getOtherData());
EXPECT_EQ(TSIGContext::SENT_RESPONSE, ctx->getState());
EXPECT_EQ(TSIGError::BAD_KEY(), ctx->getError());
}
/// @brief Check that a security context is required to sign.
TEST_F(GssTsigContextTest, signNoSecCtx) {
GssTsigKeyPtr key;
string name = "1234.sig-foo.example.com.";
ASSERT_NO_THROW(key.reset(new GssTsigKey(name)));
ASSERT_TRUE(key);
GssTsigContextPtr ctx;
ASSERT_NO_THROW(ctx.reset(new GssTsigContext(*key)));
ASSERT_TRUE(ctx);
const string& dummy = "dummy";
EXPECT_THROW_MSG(ctx->sign(0x1234, dummy.c_str(), dummy.size()),
Unexpected,
"sign called with null security context");
}
/// @brief Check that verify no TSIG record is only for continuation.
TEST_F(GssTsigContextTest, verifyNoRecord) {
GssTsigKeyPtr key;
string name = "1234.sig-foo.example.com.";
ASSERT_NO_THROW(key.reset(new GssTsigKey(name)));
ASSERT_TRUE(key);
GssTsigContextPtr ctx;
ASSERT_NO_THROW(ctx.reset(new GssTsigContext(*key)));
ASSERT_TRUE(ctx);
const string& dummy = "dummy";
TSIGError error = TSIGError::NOERROR();
EXPECT_NO_THROW(error = ctx->verify(0, dummy.c_str(), dummy.size()));
EXPECT_EQ(TSIGError::FORMERR(), error);
EXPECT_EQ(TSIGContext::RECEIVED_REQUEST, ctx->getState());
EXPECT_EQ(TSIGError::FORMERR(), ctx->getError());
}
/// @brief Check that verify rejects too short data.
TEST_F(GssTsigContextTest, verifyBadData) {
GssTsigKeyPtr key;
string name = "1234.sig-foo.example.com.";
ASSERT_NO_THROW(key.reset(new GssTsigKey(name)));
ASSERT_TRUE(key);
GssTsigContextPtr ctx;
ASSERT_NO_THROW(ctx.reset(new GssTsigContext(*key)));
ASSERT_TRUE(ctx);
const uint64_t now = static_cast<uint64_t>(time(0));
ConstTSIGRecordPtr tsig(new
TSIGRecord(key->getKeyName(),
TSIG(key->getAlgorithmName(),
now, TSIGContext::DEFAULT_FUDGE, 0, 0,
0x1234, 0, 0, 0)));
const size_t MESSAGE_HEADER_LEN = 12;
size_t len = MESSAGE_HEADER_LEN + tsig->getLength();
// len is 12 + 26 + 26 + 10 = 74.
EXPECT_EQ(74, len);
EXPECT_THROW_MSG(ctx->verify(tsig.get(), 0, len - 1), InvalidParameter,
"TSIG verify: data length is invalid: 73");
EXPECT_THROW_MSG(ctx->verify(tsig.get(), 0, len), InvalidParameter,
"TSIG verify: empty data is invalid");
}
/// @brief Check that a security context is required to verify.
TEST_F(GssTsigContextTest, verifyNoSecCtx) {
GssTsigKeyPtr key;
string name = "1234.sig-foo.example.com.";
ASSERT_NO_THROW(key.reset(new GssTsigKey(name)));
ASSERT_TRUE(key);
GssTsigContextPtr ctx;
ASSERT_NO_THROW(ctx.reset(new GssTsigContext(*key)));
ASSERT_TRUE(ctx);
const uint64_t now = static_cast<uint64_t>(time(0));
ConstTSIGRecordPtr tsig(new
TSIGRecord(key->getKeyName(),
TSIG(key->getAlgorithmName(),
now, TSIGContext::DEFAULT_FUDGE, 0, 0,
0x1234, 0, 0, 0)));
const size_t MESSAGE_HEADER_LEN = 12;
size_t len = MESSAGE_HEADER_LEN + tsig->getLength();
string dummy(len, ' ');
EXPECT_THROW_MSG(ctx->verify(tsig.get(), dummy.c_str(), len), Unexpected,
"verify called with null security context");
}
/// @brief Check that sign works as expected.
TEST_F(GssTsigContextTest, sign) {
GssTsigKeyPtr key;
string name = "1234.sig-foo.example.com.";
ASSERT_NO_THROW(key.reset(new GssTsigKey(name)));
ASSERT_TRUE(key);
setKeytab();
setAdministratorCCache();
// Server.
GssApiName srv_name("DNS/blu.example.nil@EXAMPLE.NIL");
OM_uint32 lifetime = 0;
GssApiCredPtr srv_cred(new GssApiCred(srv_name, GSS_C_ACCEPT, lifetime));
GssApiSecCtx srv_ctx(GSS_C_NO_CONTEXT);
// Client.
GssApiName clnt_name;
GssApiCredPtr cred;
EXPECT_FALSE(key->getSecCtx().get());
OM_uint32 flags = GSS_C_REPLAY_FLAG | GSS_C_MUTUAL_FLAG | GSS_C_INTEG_FLAG;
bool ret = false;
// Exchanges.
GssApiBuffer intoken0;
GssApiBuffer outtoken0;
ASSERT_NO_THROW(ret = key->getSecCtx().init(cred, srv_name, flags,
intoken0, outtoken0,
lifetime));
ASSERT_FALSE(outtoken0.empty());
GssApiBuffer outtoken1;
ASSERT_NO_THROW(ret = srv_ctx.accept(*srv_cred, outtoken0, clnt_name,
outtoken1));
EXPECT_TRUE(ret);
GssApiBuffer outtoken2;
ASSERT_NO_THROW(ret = key->getSecCtx().init(cred, srv_name, flags,
outtoken1, outtoken2,
lifetime));
ASSERT_TRUE(ret);
EXPECT_TRUE(outtoken2.empty());
// Build the message to sign.
message_.clear(Message::RENDER);
message_.setQid(0x1234);
message_.setOpcode(Opcode::QUERY());
message_.setRcode(Rcode::NOERROR());
message_.setHeaderFlag(Message::HEADERFLAG_QR);
Name qname("foo.example.nil.");
message_.addQuestion(Question(qname, RRClass::IN(), RRType::A()));
MessageRenderer renderer;
OutputBuffer buf(1024);
renderer.setBuffer(&buf);
EXPECT_NO_THROW(message_.toWire(renderer));
// len is 12 + 17 + 4 = 33
ASSERT_TRUE(buf.getData());
EXPECT_EQ(33, buf.getLength());
// Sign.
GssTsigContextPtr ctx;
ASSERT_NO_THROW(ctx.reset(new GssTsigContext(*key)));
ASSERT_TRUE(ctx);
ConstTSIGRecordPtr tsig;
uint64_t before = static_cast<uint64_t>(time(0));
EXPECT_NO_THROW(tsig = ctx->sign(0x4321, buf.getData(), buf.getLength()));
uint64_t after = static_cast<uint64_t>(time(0));
ASSERT_TRUE(tsig);
EXPECT_EQ(Name(name), tsig->getName());
EXPECT_EQ(RRClass::ANY(), tsig->getClass());
EXPECT_EQ(0, tsig->getTTL().getValue());
EXPECT_LT(62, tsig->getLength());
const TSIG& rdata = tsig->getRdata();
EXPECT_EQ(Name("gss-tsig."), rdata.getAlgorithm());
EXPECT_LE(before, rdata.getTimeSigned());
EXPECT_GE(after, rdata.getTimeSigned());
EXPECT_EQ(TSIGContext::DEFAULT_FUDGE, rdata.getFudge());
EXPECT_EQ(tsig->getLength() - 62, rdata.getMACSize());
EXPECT_TRUE(rdata.getMAC());
EXPECT_EQ(0x4321, rdata.getOriginalID());
EXPECT_EQ(0, rdata.getError());
EXPECT_EQ(0, rdata.getOtherLen());
EXPECT_FALSE(rdata.getOtherData());
EXPECT_EQ(TSIGContext::SENT_REQUEST, ctx->getState());
EXPECT_EQ(TSIGError::NOERROR(), ctx->getError());
// Depend on setup.
EXPECT_EQ(28, rdata.getMACSize());
}
/// @brief Check that toWire with sign works as expected.
TEST_F(GssTsigContextTest, signToWire) {
GssTsigKeyPtr key;
string name = "1234.sig-foo.example.com.";
ASSERT_NO_THROW(key.reset(new GssTsigKey(name)));
ASSERT_TRUE(key);
setKeytab();
setAdministratorCCache();
// Server.
GssApiName srv_name("DNS/blu.example.nil@EXAMPLE.NIL");
OM_uint32 lifetime = 0;
GssApiCredPtr srv_cred(new GssApiCred(srv_name, GSS_C_ACCEPT, lifetime));
GssApiSecCtx srv_ctx(GSS_C_NO_CONTEXT);
// Client.
GssApiName clnt_name;
GssApiCredPtr cred;
EXPECT_FALSE(key->getSecCtx().get());
OM_uint32 flags = GSS_C_REPLAY_FLAG | GSS_C_MUTUAL_FLAG | GSS_C_INTEG_FLAG;
bool ret = false;
// Exchanges.
GssApiBuffer intoken0;
GssApiBuffer outtoken0;
ASSERT_NO_THROW(ret = key->getSecCtx().init(cred, srv_name, flags,
intoken0, outtoken0,
lifetime));
ASSERT_FALSE(outtoken0.empty());
GssApiBuffer outtoken1;
ASSERT_NO_THROW(ret = srv_ctx.accept(*srv_cred, outtoken0, clnt_name,
outtoken1));
EXPECT_TRUE(ret);
GssApiBuffer outtoken2;
ASSERT_NO_THROW(ret = key->getSecCtx().init(cred, srv_name, flags,
outtoken1, outtoken2,
lifetime));
ASSERT_TRUE(ret);
EXPECT_TRUE(outtoken2.empty());
// Build the message to sign.
message_.clear(Message::RENDER);
message_.setQid(0x1234);
message_.setOpcode(Opcode::QUERY());
message_.setRcode(Rcode::NOERROR());
message_.setHeaderFlag(Message::HEADERFLAG_QR);
Name qname("foo.example.nil.");
message_.addQuestion(Question(qname, RRClass::IN(), RRType::A()));
MessageRenderer renderer;
OutputBuffer obuf(1024);
renderer.setBuffer(&obuf);
// Sign.
GssTsigContextPtr ctx;
ASSERT_NO_THROW(ctx.reset(new GssTsigContext(*key)));
ASSERT_TRUE(ctx);
uint64_t before = static_cast<uint64_t>(time(0));
EXPECT_NO_THROW(message_.toWire(renderer, ctx.get()));
uint64_t after = static_cast<uint64_t>(time(0));
// len is 33 + 62 + 28 = 123.
ASSERT_TRUE(obuf.getData());
EXPECT_EQ(123, obuf.getLength());
// Check the TSIG RR.
message_.clear(Message::PARSE);
InputBuffer ibuf(obuf.getData(), obuf.getLength());
EXPECT_NO_THROW(message_.fromWire(ibuf));
const TSIGRecord* tsig = message_.getTSIGRecord();
ASSERT_TRUE(tsig);
EXPECT_EQ(Name(name), tsig->getName());
EXPECT_EQ(RRClass::ANY(), tsig->getClass());
EXPECT_EQ(0, tsig->getTTL().getValue());
EXPECT_LT(62, tsig->getLength());
const TSIG& rdata = tsig->getRdata();
EXPECT_EQ(Name("gss-tsig."), rdata.getAlgorithm());
EXPECT_LE(before, rdata.getTimeSigned());
EXPECT_GE(after, rdata.getTimeSigned());
EXPECT_EQ(TSIGContext::DEFAULT_FUDGE, rdata.getFudge());
EXPECT_EQ(tsig->getLength() - 62, rdata.getMACSize());
EXPECT_TRUE(rdata.getMAC());
EXPECT_EQ(0x1234, rdata.getOriginalID());
EXPECT_EQ(0, rdata.getError());
EXPECT_EQ(0, rdata.getOtherLen());
EXPECT_FALSE(rdata.getOtherData());
EXPECT_EQ(TSIGContext::SENT_REQUEST, ctx->getState());
EXPECT_EQ(TSIGError::NOERROR(), ctx->getError());
// Depend on setup.
EXPECT_EQ(28, rdata.getMACSize());
}
/// @brief Check that sign and verify work as expected.
TEST_F(GssTsigContextTest, signVerify) {
GssTsigKeyPtr key;
string name = "1234.sig-foo.example.com.";
ASSERT_NO_THROW(key.reset(new GssTsigKey(name)));
ASSERT_TRUE(key);
setKeytab();
setAdministratorCCache();
// Server.
GssTsigKeyPtr srv_key;
ASSERT_NO_THROW(srv_key.reset(new GssTsigKey(name)));
ASSERT_TRUE(srv_key);
EXPECT_FALSE(srv_key->getSecCtx().get());
GssApiName srv_name("DNS/blu.example.nil@EXAMPLE.NIL");
OM_uint32 lifetime = 0;
GssApiCredPtr srv_cred(new GssApiCred(srv_name, GSS_C_ACCEPT, lifetime));
// Client.
GssApiName clnt_name;
GssApiCredPtr cred;
EXPECT_FALSE(key->getSecCtx().get());
OM_uint32 flags = GSS_C_REPLAY_FLAG | GSS_C_MUTUAL_FLAG | GSS_C_INTEG_FLAG;
bool ret = false;
// Exchanges.
GssApiBuffer intoken0;
GssApiBuffer outtoken0;
ASSERT_NO_THROW(ret = key->getSecCtx().init(cred, srv_name, flags,
intoken0, outtoken0,
lifetime));
ASSERT_FALSE(outtoken0.empty());
GssApiBuffer outtoken1;
ASSERT_NO_THROW(ret = srv_key->getSecCtx().accept(*srv_cred, outtoken0,
clnt_name, outtoken1));
EXPECT_TRUE(ret);
GssApiBuffer outtoken2;
ASSERT_NO_THROW(ret = key->getSecCtx().init(cred, srv_name, flags,
outtoken1, outtoken2,
lifetime));
ASSERT_TRUE(ret);
EXPECT_TRUE(outtoken2.empty());
// Build the message to sign.
message_.clear(Message::RENDER);
message_.setQid(0x1234);
message_.setOpcode(Opcode::QUERY());
message_.setRcode(Rcode::NOERROR());
message_.setHeaderFlag(Message::HEADERFLAG_QR);
Name qname("foo.example.nil.");
message_.addQuestion(Question(qname, RRClass::IN(), RRType::A()));
MessageRenderer renderer;
OutputBuffer obuf(1024);
renderer.setBuffer(&obuf);
// Sign.
GssTsigContextPtr ctx;
ASSERT_NO_THROW(ctx.reset(new GssTsigContext(*key)));
ASSERT_TRUE(ctx);
EXPECT_NO_THROW(message_.toWire(renderer, ctx.get()));
// len is 33 + 62 + 28 = 123.
ASSERT_TRUE(obuf.getData());
EXPECT_EQ(123, obuf.getLength());
// Check the TSIG RR.
message_.clear(Message::PARSE);
InputBuffer ibuf(obuf.getData(), obuf.getLength());
EXPECT_NO_THROW(message_.fromWire(ibuf));
const TSIGRecord* tsig = message_.getTSIGRecord();
ASSERT_TRUE(tsig);
// Verify.
ASSERT_NO_THROW(ctx.reset(new GssTsigContext(*srv_key)));
ASSERT_TRUE(ctx);
TSIGError error = TSIGError::NOERROR();
EXPECT_NO_THROW(error = ctx->verify(tsig, obuf.getData(),
obuf.getLength()));
EXPECT_EQ(TSIGError::NOERROR(), error);
EXPECT_TRUE(ctx->lastHadSignature());
EXPECT_EQ(TSIGError::NOERROR(), ctx->getError());
EXPECT_EQ(TSIGContext::RECEIVED_REQUEST, ctx->getState());
// Only once.
ASSERT_NO_THROW(ctx.reset(new GssTsigContext(*srv_key)));
ASSERT_TRUE(ctx);
EXPECT_NO_THROW(error = ctx->verify(tsig, obuf.getData(),
obuf.getLength()));
// Error message is 'The token was a duplicate of an earlier token'.
EXPECT_EQ(TSIGError::BAD_SIG(), error);
EXPECT_EQ(TSIGError::BAD_SIG(), ctx->getError());
EXPECT_EQ(TSIGContext::RECEIVED_REQUEST, ctx->getState());
EXPECT_EQ(GSS_S_DUPLICATE_TOKEN, srv_key->getSecCtx().getLastError());
// Bad key is persistent.
ASSERT_NO_THROW(ctx.reset(new GssTsigContext(*srv_key)));
ASSERT_TRUE(ctx);
ctx->setError(TSIGError::BAD_KEY());
EXPECT_NO_THROW(error = ctx->verify(tsig, obuf.getData(),
obuf.getLength()));
// No message: the error is raised before crypto.
EXPECT_EQ(TSIGError::BAD_KEY(), error);
EXPECT_EQ(TSIGError::BAD_KEY(), ctx->getError());
EXPECT_EQ(TSIGContext::RECEIVED_REQUEST, ctx->getState());
}
/// @brief Check that sign and verify fail on modified signature.
TEST_F(GssTsigContextTest, signBadVerify) {
GssTsigKeyPtr key;
string name = "1234.sig-foo.example.com.";
ASSERT_NO_THROW(key.reset(new GssTsigKey(name)));
ASSERT_TRUE(key);
setKeytab();
setAdministratorCCache();
// Server.
GssTsigKeyPtr srv_key;
ASSERT_NO_THROW(srv_key.reset(new GssTsigKey(name)));
ASSERT_TRUE(srv_key);
EXPECT_FALSE(srv_key->getSecCtx().get());
GssApiName srv_name("DNS/blu.example.nil@EXAMPLE.NIL");
OM_uint32 lifetime = 0;
GssApiCredPtr srv_cred(new GssApiCred(srv_name, GSS_C_ACCEPT, lifetime));
// Client.
GssApiName clnt_name;
GssApiCredPtr cred;
EXPECT_FALSE(key->getSecCtx().get());
OM_uint32 flags = GSS_C_REPLAY_FLAG | GSS_C_MUTUAL_FLAG | GSS_C_INTEG_FLAG;
bool ret = false;
// Exchanges.
GssApiBuffer intoken0;
GssApiBuffer outtoken0;
ASSERT_NO_THROW(ret = key->getSecCtx().init(cred, srv_name, flags,
intoken0, outtoken0,
lifetime));
ASSERT_FALSE(outtoken0.empty());
GssApiBuffer outtoken1;
ASSERT_NO_THROW(ret = srv_key->getSecCtx().accept(*srv_cred, outtoken0,
clnt_name, outtoken1));
EXPECT_TRUE(ret);
GssApiBuffer outtoken2;
ASSERT_NO_THROW(ret = key->getSecCtx().init(cred, srv_name, flags,
outtoken1, outtoken2,
lifetime));
ASSERT_TRUE(ret);
EXPECT_TRUE(outtoken2.empty());
// Build the message to sign.
message_.clear(Message::RENDER);
message_.setQid(0x1234);
message_.setOpcode(Opcode::QUERY());
message_.setRcode(Rcode::NOERROR());
message_.setHeaderFlag(Message::HEADERFLAG_QR);
Name qname("foo.example.nil.");
message_.addQuestion(Question(qname, RRClass::IN(), RRType::A()));
MessageRenderer renderer;
OutputBuffer obuf(1024);
renderer.setBuffer(&obuf);
// Sign.
GssTsigContextPtr ctx;
ASSERT_NO_THROW(ctx.reset(new GssTsigContext(*key)));
ASSERT_TRUE(ctx);
EXPECT_NO_THROW(message_.toWire(renderer, ctx.get()));
// len is 33 + 62 + 28 = 123.
ASSERT_TRUE(obuf.getData());
EXPECT_EQ(123, obuf.getLength());
// Check the TSIG RR.
message_.clear(Message::PARSE);
InputBuffer ibuf(obuf.getData(), obuf.getLength());
// Change the signature at 93..121.
ASSERT_EQ(123, obuf.getLength());
const uint8_t* ptr = obuf.getData();
obuf.writeUint8At(ptr[120] ^ 1, 120);
EXPECT_NO_THROW(message_.fromWire(ibuf));
const TSIGRecord* tsig = message_.getTSIGRecord();
ASSERT_TRUE(tsig);
// Bad verify.
ASSERT_NO_THROW(ctx.reset(new GssTsigContext(*srv_key)));
ASSERT_TRUE(ctx);
TSIGError error = TSIGError::NOERROR();
EXPECT_NO_THROW(error = ctx->verify(tsig, obuf.getData(),
obuf.getLength()));
// Error message is 'A token had an invalid Message Integrity Check (MIC)'.
EXPECT_EQ(TSIGError::BAD_SIG(), error);
EXPECT_EQ(TSIGError::BAD_SIG(), ctx->getError());
EXPECT_EQ(TSIGContext::RECEIVED_REQUEST, ctx->getState());
EXPECT_EQ(GSS_S_BAD_SIG, srv_key->getSecCtx().getLastError());
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,68 @@
// Copyright (C) 2021-2022 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 <dns/name.h>
#include <gss_tsig_key.h>
#include <gss_tsig_api_utils.h>
#include <testutils/gtest_utils.h>
#include <gtest/gtest.h>
using namespace std;
using namespace std::chrono;
using namespace isc;
using namespace isc::cryptolink;
using namespace isc::dns;
using namespace isc::gss_tsig;
using namespace isc::gss_tsig::test;
namespace {
/// @brief Test fixture for testing the GSS-TSIG key.
class GssTsigKeyTest : public GssApiBaseTest {
public:
/// @brief Constructor.
GssTsigKeyTest() : GssApiBaseTest() {
}
};
/// @brief Check the constructor builds what is expected.
TEST_F(GssTsigKeyTest, basic) {
GssTsigKeyPtr key;
string name = "1234.sig-foo.example.com.";
ASSERT_NO_THROW(key.reset(new GssTsigKey(name)));
ASSERT_TRUE(key);
EXPECT_EQ(Name(name), key->getKeyName());
EXPECT_EQ(name, key->getKeyName().toText());
EXPECT_EQ(Name("gss-tsig."), key->getAlgorithmName());
EXPECT_EQ(TSIGKey::GSSTSIG_NAME(), key->getAlgorithmName());
EXPECT_EQ(UNKNOWN_HASH, key->getAlgorithm());
EXPECT_EQ(0, key->getDigestbits());
EXPECT_EQ(0, key->getSecretLength());
EXPECT_FALSE(key->getSecret());
string expected = name + "::gss-tsig.";
EXPECT_EQ(expected, key->toText());
EXPECT_FALSE(key->getSecCtx().get());
system_clock::time_point epoch;
system_clock::time_point now = system_clock::now();
uint32_t now32 = static_cast<uint32_t>(system_clock::to_time_t(now));
EXPECT_EQ(epoch, key->getInception());
EXPECT_EQ(0, key->getInception32());
EXPECT_EQ(epoch, key->getExpire());
EXPECT_EQ(0, key->getExpire32());
EXPECT_NO_THROW(key->setInception(now));
EXPECT_EQ(now, key->getInception());
EXPECT_EQ(now32, key->getInception32());
std::chrono::hours day(24);
system_clock::time_point tomorrow = now + day;
uint32_t tomorrow32 = static_cast<uint32_t>(system_clock::to_time_t(tomorrow));
EXPECT_NO_THROW(key->setExpire(tomorrow));
EXPECT_EQ(tomorrow, key->getExpire());
EXPECT_EQ(tomorrow32, key->getExpire32());
}
}

View File

@ -0,0 +1,175 @@
// Copyright (C) 2021-2022 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 <dns/name.h>
#include <gss_tsig_context.h>
#include <managed_key.h>
#include <testutils/gtest_utils.h>
#include <testutils/test_to_element.h>
#include <util/chrono_time_utils.h>
#include <gtest/gtest.h>
using namespace std;
using namespace std::chrono;
using namespace isc;
using namespace isc::data;
using namespace isc::dns;
using namespace isc::gss_tsig;
using namespace isc::test;
using namespace isc::util;
namespace {
/// @brief Check managed GSS-TSIG key.
TEST(ManagedKeyTest, basic) {
ManagedKeyPtr key;
string name = "1234.sig-foo.example.com.";
ASSERT_NO_THROW(key.reset(new ManagedKey(name)));
ASSERT_TRUE(key);
EXPECT_EQ(Name(name), key->getKeyName());
EXPECT_EQ(name, key->getKeyNameStr());
// Parent.
EXPECT_EQ("", key->getParentID());
EXPECT_NO_THROW(key->setParentID("foo"));
EXPECT_EQ("foo", key->getParentID());
// Status.
EXPECT_EQ(ManagedKey::NOT_READY, key->getStatus());
EXPECT_NO_THROW(key->setStatus(ManagedKey::USABLE));
EXPECT_EQ(ManagedKey::USABLE, key->getStatus());
// TKEY status.
EXPECT_EQ(TKeyExchange::OTHER, key->getTKeyStatus());
EXPECT_NO_THROW(key->setTKeyStatus(TKeyExchange::IO_STOPPED));
EXPECT_EQ(TKeyExchange::IO_STOPPED, key->getTKeyStatus());
// TKEY exchange.
EXPECT_FALSE(key->getTKeyExchange());
// Set inception and expire.
system_clock::time_point now = system_clock::now();
EXPECT_NO_THROW(key->setInception(now));
std::chrono::hours day(24);
system_clock::time_point tomorrow = now + day;
EXPECT_NO_THROW(key->setExpire(tomorrow));
// TKEY completion handler.
// Success.
EXPECT_NO_THROW((*key)(TKeyExchange::SUCCESS));
EXPECT_EQ(TKeyExchange::SUCCESS, key->getTKeyStatus());
EXPECT_EQ(ManagedKey::USABLE, key->getStatus());
// Others.
EXPECT_NO_THROW((*key)(TKeyExchange::TIMEOUT));
EXPECT_EQ(TKeyExchange::TIMEOUT, key->getTKeyStatus());
EXPECT_EQ(ManagedKey::IN_ERROR, key->getStatus());
EXPECT_NO_THROW((*key)(TKeyExchange::IO_STOPPED));
EXPECT_EQ(TKeyExchange::IO_STOPPED, key->getTKeyStatus());
EXPECT_EQ(ManagedKey::IN_ERROR, key->getStatus());
EXPECT_NO_THROW((*key)(TKeyExchange::INVALID_RESPONSE));
EXPECT_EQ(TKeyExchange::INVALID_RESPONSE, key->getTKeyStatus());
EXPECT_EQ(ManagedKey::IN_ERROR, key->getStatus());
EXPECT_NO_THROW((*key)(TKeyExchange::UNSIGNED_RESPONSE));
EXPECT_EQ(TKeyExchange::UNSIGNED_RESPONSE, key->getTKeyStatus());
EXPECT_EQ(ManagedKey::IN_ERROR, key->getStatus());
EXPECT_NO_THROW((*key)(TKeyExchange::BAD_CREDENTIALS));
EXPECT_EQ(TKeyExchange::BAD_CREDENTIALS, key->getTKeyStatus());
EXPECT_EQ(ManagedKey::IN_ERROR, key->getStatus());
EXPECT_NO_THROW((*key)(TKeyExchange::OTHER));
EXPECT_EQ(TKeyExchange::OTHER, key->getTKeyStatus());
EXPECT_EQ(ManagedKey::IN_ERROR, key->getStatus());
// Note that the EXPIRED status is set by users and is in fact
// only a shortcut i.e. can be removed leaving expire > now check.
}
/// @brief Check managed GSS-TSIG key toElement method.
TEST(ManagedKeyTest, toElement) {
ManagedKeyPtr key;
const string& name("1234.sig-foo.example.com.");
ASSERT_NO_THROW(key.reset(new ManagedKey(name)));
const string& id("foo");
key->setParentID(id);
system_clock::time_point now = system_clock::now();
key->setInception(now);
std::chrono::hours day(24);
system_clock::time_point tomorrow = now + day;
key->setExpire(tomorrow);
ElementPtr expected = Element::createMap();
expected->set("name", Element::create(name));
expected->set("server-id", Element::create(id));
expected->set("status", Element::create(string("not yet ready")));
expected->set("tkey-exchange", Element::create(false));
expected->set("inception-date", Element::create(clockToText(now)));
expected->set("expire-date", Element::create(clockToText(tomorrow)));
runToElementTest<ManagedKey>(expected, *key);
expected->remove("tkey-exchange");
// Usable.
key->setStatus(ManagedKey::USABLE);
expected->set("status", Element::create(string("usable")));
runToElementTest<ManagedKey>(expected, *key);
// Expired.
key->setStatus(ManagedKey::EXPIRED);
expected->set("status", Element::create(string("expired")));
runToElementTest<ManagedKey>(expected, *key);
// Error (timeout).
key->setStatus(ManagedKey::IN_ERROR);
key->setTKeyStatus(TKeyExchange::TIMEOUT);
expected->set("status", Element::create(string("in error")));
expected->set("tkey-status",
Element::create(string("no response, timeout")));
runToElementTest<ManagedKey>(expected, *key);
}
/// @brief name generation.
TEST(ManagedKeyTest, genName) {
string server = "foo.bar";
string name = ManagedKey::genName(server);
size_t dot = name.find_first_of(".");
ASSERT_NE(dot, string::npos);
EXPECT_EQ(server, name.substr(dot + 1));
string name2 = ManagedKey::genName(server);
EXPECT_NE(name, name2);
dot = name2.find_first_of(".");
ASSERT_NE(dot, string::npos);
EXPECT_EQ(server, name2.substr(dot + 1));
string name3 = ManagedKey::genName(server);
EXPECT_NE(name, name3);
EXPECT_NE(name2, name3);
}
/// @brief create Context.
TEST(ManagedKeyTest, createContext) {
ManagedKeyPtr key;
string name = "1234.sig-foo.example.com.";
ASSERT_NO_THROW(key.reset(new ManagedKey(name)));
ASSERT_TRUE(key);
// Now create the context.
TSIGContextPtr octx;
ASSERT_NO_THROW(octx = key->createContext());
ASSERT_TRUE(octx);
EXPECT_EQ(TSIGContext::INIT, octx->getState());
EXPECT_EQ(TSIGError::NOERROR(), octx->getError());
GssTsigContextPtr ctx = boost::dynamic_pointer_cast<GssTsigContext>(octx);
ASSERT_TRUE(ctx);
}
}

View File

@ -0,0 +1,30 @@
if not gtest.found() or not krb5.found()
subdir_done()
endif
current_source_dir = meson.current_source_dir()
ddns_gss_tsig_tests_libs = [
ddns_gss_tsig_testutils_lib,
ddns_gss_tsig_archive,
kea_d2srv_testutils_lib,
kea_testutils_lib,
]
ddns_gss_tsig_tests = executable(
'ddns-gss-tsig-tests',
'dns_update_unittests.cc',
'gss_tsig_api_unittests.cc',
'gss_tsig_callouts_unittests.cc',
'gss_tsig_cfg_unittests.cc',
'gss_tsig_context_unittests.cc',
'gss_tsig_impl_unittests.cc',
'gss_tsig_key_unittests.cc',
'managed_key_unittests.cc',
'run_unittests.cc',
'tkey_exchange_unittests.cc',
'tkey_unittests.cc',
cpp_args: [f'-DTEST_DATA_DIR="@current_source_dir@"'],
dependencies: [krb5, gtest, crypto],
include_directories: [include_directories('.'), include_directories('..')] + INCLUDES,
link_with: [ddns_gss_tsig_tests_libs] + LIBS_BUILT_SO_FAR,
)
test('ddns-gss-tsig-tests', ddns_gss_tsig_tests, protocol: 'gtest')

View File

@ -0,0 +1,669 @@
// Copyright (C) 2021-2024 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 <asiodns/io_fetch.h>
#include <cryptolink/crypto_rng.h>
#include <dns/messagerenderer.h>
#include <dns/opcode.h>
#include <log/logger_support.h>
#include <stats/stats_mgr.h>
#include <gss_tsig_api.h>
#include <gss_tsig_context.h>
#include <gss_tsig_key.h>
#include <cstring>
#include <iostream>
#include <limits>
using namespace isc;
using namespace isc::asiodns;
using namespace isc::asiolink;
using namespace isc::dns;
using namespace isc::dns::rdata;
using namespace isc::dns::rdata::generic;
using namespace isc::gss_tsig;
using namespace isc::stats;
using namespace isc::util;
using namespace std;
// Parameters decoded in main() from the command line.
/// @brief DNS server address (-s <addr>[/<port>]).
string named_addr("10.53.0.1");
/// @brief DNS server port (s argument).
uint16_t named_port(5300);
/// @brief DNS server principal (-p <princ>).
string named_princ("DNS/blu.example.nil@EXAMPLE.NIL");
/// @brief Key name (-d <key-domain> and -k <full-key-name>).
string key_name("sig-blu.example.nil.");
/// @brief Randomize key name (no k flag).
bool rnd_key_name(true);
/// @brief Client credential principal (-c <cred>).
string cred_princ("");
/// @brief Client credential cache specification (-C <ccache>).
string ccache_spec = string("FILE:") + string(TEST_DATA_DIR) +
string("/administrator.ccache");
/// @brief Name to update (-n <name>).
string upd_name("testdc1.example.nil.");
/// @brief Zone to update (-z <zone>).
string upd_zone("example.nil.");
/// @brief Address of update (-a <addr4>).
string upd_addr("10.53.0.10");
/// @brief Time To Live of update (one day) (-t <ttl>).
uint32_t upd_ttl(86400);
/// @brief TKEY key lifetime (one hour) (-l <lifetime).
uint32_t tkey_lifetime(3600);
/// @brief I/O timeout (2 seconds) (-w <wait>).
unsigned io_wait(2000);
/// @brief Query ID base (-q <qid>).
///
/// @note IOFetch puts a random qid in outgoing messages.
uint16_t qid(0x1234);
/// @brief GSS-API flags (-f <flags>).
OM_uint32 flags(GSS_C_REPLAY_FLAG | GSS_C_MUTUAL_FLAG | GSS_C_INTEG_FLAG);
/// @brief IOFetch protocol (-u).
IOFetch::Protocol protocol(IOFetch::TCP);
/// @brief Verbose flag.
bool verbose(false);
// Work values.
/// @brief GSS-TSIG Key.
GssTsigKeyPtr key;
/// @brief GSS-TSIG credential.
GssApiCredPtr cred;
/// @brief GSS-TSIG TKEY context.
GssTsigContextPtr tkey_ctx;
/// @brief GSS-TSIG update context.
GssTsigContextPtr upd_ctx;
/// @brief Input token.
GssApiBufferPtr intoken;
/// @brief Output token.
GssApiBufferPtr outtoken;
/// @brief DNS message.
MessagePtr msg;
/// @brief DNS message buffer.
const void* msg_buf(0);
/// @brief DNS message length.
size_t msg_len(0);
/// @brief TKEY RRset.
RRsetPtr tkey_rrset;
/// @brief TKEY Rdata.
ConstRdataPtr tkey_rdata;
/// @brief Update RRset.
RRsetPtr upd_rrset;
/// @brief Update Rdata.
ConstRdataPtr upd_rdata;
/// @brief Current time.
time_t now(0);
/// @brief I/O service.
IOServicePtr io_service;
/// @brief I/O fetch result.
IOFetch::Result io_result;
/// @brief I/O fetch done flag.
bool io_done(false);
/// @brief I/O fetch callback class.
class IOCallback : public IOFetch::Callback {
public:
/// @brief Callback operator.
void operator()(IOFetch::Result result) {
io_done = true;
io_result = result;
}
};
/// @brief I/O fetch callback object.
IOCallback cb;
/// @brief Clean and exit function.
[[noreturn]] void finish(int exit_code) {
try {
StatsMgr::instance().removeAll();
} catch (...) {
// Force an error.
exit_code = 1;
}
exit(exit_code);
}
/// @brief Setup function.
void setup() {
log::initLogger();
// Usual sanity check for IOFetch wait argument.
if (io_wait > numeric_limits<int>::max()) {
cerr << "I/O timeout value " << io_wait << " is too large\n";
finish(1);
}
// Command line setting to '' means it was already set.
if (!ccache_spec.empty()) {
setenv("KRB5CCNAME", ccache_spec.c_str(), 1);
}
// Create a random key name when not set from the command line.
// The DNS server does not allow to reuse a key name.
if (rnd_key_name) {
uint32_t n;
vector<uint8_t> r = isc::cryptolink::random(sizeof(uint32_t));
memmove(&n, &r[0], sizeof(uint32_t));
ostringstream s;
s << n << "." << key_name;
key_name = s.str();
}
key.reset(new GssTsigKey(key_name));
// Get the credential when set from the command line.
if (!cred_princ.empty()) {
OM_uint32 lifetime(0);
GssApiName cname(cred_princ);
cred.reset(new GssApiCred(cname, GSS_C_INITIATE, lifetime));
if (lifetime == 0) {
cerr << "credential for '" << cred_princ << "' already expired\n";
}
}
io_service.reset(new IOService());
}
/// @brief TKEY exchange(s).
void tkey() {
GssApiName named_gname(named_princ);
bool ret(false);
intoken.reset(new GssApiBuffer());
OutputBufferPtr outbuf;
// GSS-API security context establishment using TKEY to transport them.
for (;;) {
outtoken.reset(new GssApiBuffer());
OM_uint32 lifetime = 0;
ret = key->getSecCtx().init(cred, named_gname, flags, *intoken,
*outtoken, lifetime);
if (ret) {
// Established.
break;
}
if (outtoken->empty()) {
cerr << "output token is empty\n";
finish(1);
}
// Create a TKEY request.
msg.reset(new Message(Message::RENDER));
msg->setQid(++qid);
msg->setOpcode(Opcode::QUERY());
msg->setRcode(Rcode::NOERROR());
msg->setHeaderFlag(Message::HEADERFLAG_QR, false);
msg->setHeaderFlag(Message::HEADERFLAG_RD, false);
if (msg->getRRCount(Message::SECTION_QUESTION) > 0) {
msg->clearSection(Message::SECTION_QUESTION);
}
Name qname(key_name);
msg->addQuestion(Question(qname, RRClass::ANY(), RRType::TKEY()));
// Create the TKEY Resource Record.
Name key_dns_name(key_name);
tkey_rrset.reset(new RRset(key_dns_name, RRClass::ANY(),
RRType::TKEY(), RRTTL(0)));
Name algorithm("gss-tsig.");
now = time(0);
uint32_t inception = static_cast<uint32_t>(now);
uint32_t expire = inception + tkey_lifetime;
uint16_t mode = TKEY::GSS_API_MODE;
uint16_t error = Rcode::NOERROR().getCode();
uint16_t key_len = static_cast<uint16_t>(outtoken->getLength());
tkey_rdata.reset(new TKEY(algorithm, inception, expire, mode, error,
key_len, outtoken->getValue(), 0, 0));
tkey_rrset->addRdata(tkey_rdata);
msg->addRRset(Message::SECTION_ADDITIONAL, tkey_rrset);
// Encode the TKEY request.
MessageRenderer renderer;
OutputBufferPtr inbuf(new OutputBuffer(4096));
renderer.setBuffer(inbuf.get());
renderer.setLengthLimit(4096);
msg->toWire(renderer);
if (verbose) {
cout << "sending TKEY message " << inbuf->getLength() << "\n";
}
// Send the TKEY request.
IOAddress named_io_addr(named_addr);
outbuf.reset(new OutputBuffer(4096));
io_result = IOFetch::NOTSET;
io_done = false;
// Do not use msg because IOFetch code will mess it!
IOFetch io_fetch(protocol, io_service, inbuf, named_io_addr,
named_port, outbuf, &cb, static_cast<int>(io_wait));
io_service->post(io_fetch);
// Wait for the TKEY response.
while (!io_done) {
io_service->runOne();
}
switch (io_result) {
case IOFetch::SUCCESS:
break;
case IOFetch::TIME_OUT:
cerr << "TKEY I/O timeout\n";
finish(1);
default:
cerr << "TKEY I/O error " << io_result << "\n";
finish(1);
}
// Decode the TKEY response.
msg_len = outbuf->getLength();
if (msg_len == 0) {
cerr << "TKEY empty response\n";
finish(1);
}
msg_buf = outbuf->getData();
if (!msg_buf) {
cerr << "TKEY null response\n";
finish(1);
}
if (verbose) {
cout << "received TKEY message " << msg_len << "\n";
}
msg.reset(new Message(Message::PARSE));
InputBuffer recv_buf(msg_buf, msg_len);
msg->fromWire(recv_buf);
// Validate the TKEY response.
if (!msg->getHeaderFlag(Message::HEADERFLAG_QR)) {
cerr << "TKEY response is not a response\n";
}
if (msg->getRcode() != Rcode::NOERROR()) {
cerr << "TKEY response error "
<< msg->getRcode().toText() << "\n";
finish(1);
}
if (msg->getOpcode() != Opcode::QUERY()) {
cerr << "TKEY response opcode "
<< msg->getRcode().toText() << "\n";
finish(1);
}
if (msg->getRRCount(Message::SECTION_ANSWER) != 1) {
cerr << "TKEY response answer count "
<< msg->getRRCount(Message::SECTION_ANSWER) << "\n";
finish(1);
}
RRsetPtr rrset = *msg->beginSection(Message::SECTION_ANSWER);
if (!rrset) {
cerr << "TKEY response get answer failure\n";
finish(1);
}
if (rrset->getClass() != RRClass::ANY()) {
cerr << "TKEY response answer class "
<< rrset->getClass().toText() << "\n";
}
if (rrset->getType() != RRType::TKEY()) {
cerr << "TKEY response answer type "
<< rrset->getType().toText() << "\n";
finish(1);
}
if (rrset->getTTL() != RRTTL(0)) {
cerr << "TKEY response answer TTL "
<< rrset->getTTL().toText() << "\n";
}
if (rrset->getRdataCount() != 1) {
cerr << "TKEY response answer rdata count "
<< rrset->getRdataCount() << "\n";
if (rrset->getRdataCount() == 0) {
finish(1);
}
}
auto rdata_it = rrset->getRdataIterator();
const TKEY& tkey = dynamic_cast<const TKEY&>(rdata_it->getCurrent());
if (tkey.getError() != Rcode::NOERROR_CODE) {
cerr << "TKEY error " << tkey.getError() << "\n";
finish(1);
}
intoken.reset(new GssApiBuffer(tkey.getKeyLen(), tkey.getKey()));
if (intoken->empty()) {
cerr << "input token is empty\n";
finish(1);
}
// The TKEY payload from the server response is used for
// the GSS-API security context establishment.
}
if (!outtoken->empty()) {
// The RFC is not consistent about this case because it specifies it
// and at the same time requires the server to sign the last response
// so the first security context to be established is the server one
// and any further exchange does not make sense...
cerr << "output token is not empty\n";
}
if (verbose) {
cout << "Got GSS-API security context for "
<< key->getSecCtx().getLifetime() << " seconds\n";
}
// The last TKEY response must be signed.
const TSIGRecord* tsig = msg->getTSIGRecord();
if (!tsig) {
cerr << "last TKEY response is not signed\n";
finish(1);
}
tkey_ctx.reset(new GssTsigContext(*key));
tkey_ctx->setState(TSIGContext::SENT_REQUEST);
TSIGError error = tkey_ctx->verify(tsig, msg_buf, msg_len);
if (error != TSIGError::NOERROR()) {
cerr << "last TKEY response failed to verify\n";
finish(1);
}
if (verbose) {
cout << "verified TKEY response\n";
}
}
/// @brief Update function.
void update() {
// Build the Update request.
msg.reset(new Message(Message::RENDER));
msg->setQid(++qid);
msg->setOpcode(Opcode::UPDATE());
msg->setRcode(Rcode::NOERROR());
msg->setHeaderFlag(Message::HEADERFLAG_QR, false);
msg->setHeaderFlag(Message::HEADERFLAG_RD, false);
if (msg->getRRCount(Message::SECTION_QUESTION) > 0) {
msg->clearSection(Message::SECTION_QUESTION);
}
Name zname(upd_zone);
/// Zone section is question.
msg->addQuestion(Question(zname, RRClass::IN(), RRType::SOA()));
// Build the A Resource Record to update.
Name upd_dns_name(upd_name);
RRTTL ttl(upd_ttl);
upd_rrset.reset(new RRset(upd_dns_name, RRClass::IN(), RRType::A(), ttl));
upd_rdata.reset(new in::A(upd_addr));
upd_rrset->addRdata(upd_rdata);
// Update section is authority.
msg->addRRset(Message::SECTION_AUTHORITY, upd_rrset);
// Encode the Update request.
MessageRenderer renderer;
OutputBufferPtr inbuf(new OutputBuffer(1024));
renderer.setBuffer(inbuf.get());
upd_ctx.reset(new GssTsigContext(*key));
msg->toWire(renderer, upd_ctx.get());
if (verbose) {
cout << "sending Update message " << inbuf->getLength() << "\n";
}
// Send the Update request.
IOAddress named_io_addr(named_addr);
OutputBufferPtr outbuf(new OutputBuffer(1024));
io_result = IOFetch::NOTSET;
io_done = false;
IOFetch io_fetch(protocol, io_service, inbuf, named_io_addr,
named_port, outbuf, &cb, static_cast<int>(io_wait));
io_service->post(io_fetch);
// Wait for the Update response.
while (!io_done) {
io_service->runOne();
}
switch (io_result) {
case IOFetch::SUCCESS:
break;
case IOFetch::TIME_OUT:
cerr << "Update I/O timeout\n";
finish(1);
default:
cerr << "Update I/O error " << io_result << "\n";
finish(1);
}
// Decode the Update response.
msg_len = outbuf->getLength();
if (msg_len == 0) {
cerr << "Update empty response\n";
finish(1);
}
msg_buf = outbuf->getData();
if (!msg_buf) {
cerr << "Update null response\n";
finish(1);
}
if (verbose) {
cout << "received Update message " << msg_len << "\n";
}
msg.reset(new Message(Message::PARSE));
InputBuffer recv_buf(msg_buf, msg_len);
msg->fromWire(recv_buf);
// The Update response must be signed.
const TSIGRecord* tsig = msg->getTSIGRecord();
if (!tsig) {
cerr << "Update response is not signed\n";
finish(1);
}
TSIGError error = upd_ctx->verify(tsig, msg_buf, msg_len);
if (error != TSIGError::NOERROR()) {
cerr << "Update response failed to verify\n";
finish(1);
}
if (verbose) {
cout << "verified Update response\n";
}
finish(0);
}
/// @brief Prints usage and exists.
///
/// @note This function never returns. It terminates the process.
///
/// @param ret Return value.
void
usage(int ret) {
cerr << "Usage: " << "nsupdate"
<< " -a <addr4>"
<< " -c <cred>"
<< " -C <ccache>"
<< " -d <key-domain>"
<< " -f <flags>"
<< " -h"
<< " -k <full-key-name>"
<< " -l <lifetime>"
<< " -n <name>"
<< " -p <princ>"
<< " -q <qid>"
<< " -s <addr>[/<port>]"
<< " -t <ttl>"
<< " -u"
<< " -v"
<< " -w <wait>"
<< " -z <zone>"
<< "\n";
cerr << "Defaults are:\n"
<< "\tDNS server (-s) 10.53.0.1/5300\n"
<< "\tDNS server principal (-p) 'DNS/blu.example.nil@EXAMPLE.NIL'\n"
<< "\tClient credential principal (-c) none: use default\n"
<< "\tKey domain (-d) 'blu.example.nil.'\n"
<< "\tKey name (-k) '<random>.sig-blu.example.nil.'\n"
<< "\tCredential cache (-C) 'FILE:<pwd>/administrator.ccache'\n"
<< "\tName to update (-n) 'testdc1.example.nil.'\n"
<< "\tZone to update (-z) 'example.nil.'\n"
<< "\tIPv4 address to update (-a) '10.53.0.10'\n"
<< "\tTime To Live of update (-t) '86400' (one day)\n"
<< "\tTKEY key lifetime (-l) '3600' (one hour)\n"
<< "\tI/O timeout (-w) '2000' (in milliseconds)\n"
<< "\tQuery ID base (-q) '0x1234'\n"
<< "\tGSS-API flags (-f) '0x26'\n"
<< "\tI/O protocol (-u) TCP\n"
<< "\tverbose (-v) off\n"
<< "\thelp (-h)\n";
exit(ret);
}
/// @brief The main routine.
///
/// It decodes the command line arguments, setup work values and
/// performs TKEY and Update exchanges.
int
main(int argc, char* argv[]) {
int ch;
bool set_key_name(false);
// A bit too C style but exception free...
while ((ch = getopt(argc, argv,
"a:c:C:d:f:hk:l:n:p:q:s:t:T:uvw:z:")) != -1) {
size_t pos;
switch (ch) {
case 'a':
upd_addr = optarg;
break;
case 'c':
cred_princ = optarg;
break;
case 'C':
ccache_spec = optarg;
break;
case 'd':
if (set_key_name) {
cerr << "key name was already set\n";
usage(1);
}
set_key_name = true;
key_name = optarg;
break;
case 'f':
flags = atol(optarg);
if (flags == 0) {
cerr << "can't parse -f flags: " << optarg << "\n";
usage(1);
}
break;
case 'h':
case '?':
usage(0);
break;
case 'k':
if (set_key_name) {
cerr << "key name was already set\n";
usage(1);
}
set_key_name = true;
key_name = optarg;
rnd_key_name = false;
break;
case 'l':
tkey_lifetime = atol(optarg);
if (flags == 0) {
cerr << "can't parse -l lifetime: " << optarg << "\n";
usage(1);
}
break;
case 'n':
upd_name = optarg;
break;
case 'p':
named_princ = optarg;
break;
case 'q':
qid = atol(optarg);
if (qid == 0) {
cerr << "can't parse -q qid: " << optarg << "\n";
usage(1);
}
break;
case 's':
named_addr = optarg;
pos = named_addr.find_first_of('/');
if ((pos != string::npos) && (pos < named_addr.size())) {
string port_str = named_addr.substr(pos + 1);
named_addr = named_addr.substr(0, pos);
long port = atol(port_str.c_str());
if ((port <= 0) || (port > numeric_limits<uint16_t>::max())) {
cerr << "can't parse -s addr/port: " << optarg << "\n";
usage(1);
}
named_port = static_cast<uint16_t>(port);
}
break;
case 'u':
protocol = IOFetch::UDP;
break;
case 'v':
verbose = true;
break;
case 'w':
io_wait = atol(optarg);
if (io_wait == 0) {
cerr << "can't parse -w wait: " << optarg << "\n";
usage(1);
}
break;
case 'z':
upd_zone = optarg;
break;
default:
usage(1);
}
}
try {
setup();
tkey();
update();
} catch (const exception& ex) {
cerr << "Failed with " << ex.what() << "\n";
}
finish(1);
}

View File

@ -0,0 +1,19 @@
// Copyright (C) 2021-2022 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);
}

Binary file not shown.

View File

@ -0,0 +1,566 @@
// Copyright (C) 2021-2024 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.h>
#include <d2srv/testutils/stats_test_utils.h>
#include <dns/messagerenderer.h>
#include <dns/opcode.h>
#include <dns/rdataclass.h>
#include <gss_tsig_api_utils.h>
#include <testutils/gss_tsig_dns_server.h>
#include <gtest/gtest.h>
using namespace isc;
using namespace isc::asiodns;
using namespace isc::asiolink;
using namespace isc::d2::test;
using namespace isc::dns;
using namespace isc::dns::rdata;
using namespace isc::dns::rdata::generic;
using namespace isc::gss_tsig;
using namespace isc::gss_tsig::test;
using namespace isc::stats;
using namespace isc::stats::test;
using namespace isc::util;
using namespace std;
using namespace boost::asio;
using namespace boost::asio::ip;
namespace ph = std::placeholders;
namespace {
const char* TEST_ADDRESS = "127.0.0.1";
const uint16_t TEST_PORT = 5376;
const long TEST_TIMEOUT = 5 * 1000; // expressed in milliseconds.
/// @brief Callback class used to handle the TKEY exchange status.
class TKeyExchangeTestCallback : public TKeyExchange::Callback {
public:
/// @brief Constructor
///
/// @param io_service The IOService which handles IO operations.
/// @param status The expected status.
TKeyExchangeTestCallback(IOServicePtr io_service,
TKeyExchange::Status status, bool go_on = false) :
io_service_(io_service), status_(status), go_on_(go_on) {
}
/// @brief Destructor.
~TKeyExchangeTestCallback() = default;
/// @brief The callback function which retrieves the TKEY exchange status.
void operator()(TKeyExchange::Status status) {
if (status != status_) {
ADD_FAILURE() << "key exchange returned " << status
<< ", expected: " << status_;
}
if (!go_on_) {
io_service_->stop();
}
}
/// @brief The IOService which handles IO operations.
IOServicePtr io_service_;
/// @brief The expected TKEY exchange status.
TKeyExchange::Status status_;
/// @brief The flag which specifies if the callback should leave the
/// IOService handle more events.
bool go_on_;
};
/// @brief Test fixture for testing the GSS-API GSS-TSIG TKEY exchange with
/// Kerberos 5.
class TKeyExchangeTest : public GssApiBaseTest, public D2StatTest {
public:
/// @brief Constructor.
TKeyExchangeTest() : io_service_(new IOService()), test_timer_(io_service_) {
// Set the test timeout to break any running tasks if they hang.
test_timer_.setup(std::bind(&TKeyExchangeTest::testTimeoutHandler, this),
TEST_TIMEOUT);
}
/// @brief Destructor.
~TKeyExchangeTest() {
test_timer_.cancel();
io_service_->stopAndPoll();
}
/// @brief Handler invoked when test timeout is hit
///
/// This callback stops all running (hanging) tasks on IO service.
void testTimeoutHandler() {
io_service_->stop();
FAIL() << "Test timeout hit.";
}
/// @brief Compares StatsMgr server statistics against expected values.
///
/// Prepend server part of names before calling checkStats simpler variant.
///
/// @param serve The server.
/// @param expected_stats Map of expected static names and values.
void checkStats(const DnsServerPtr& server, const StatMap& expected_stats) {
StatMap key_stats;
for (auto const& it : expected_stats) {
const string& stat_name =
StatsMgr::generateName("server", server->getID(), it.first);
key_stats[stat_name] = it.second;
}
isc::stats::test::checkStats(key_stats);
}
/// @brief The IOService which handles IO operations.
IOServicePtr io_service_;
/// @brief The timeout timer.
asiolink::IntervalTimer test_timer_;
/// @brief Dummy DNS server.
boost::shared_ptr<DummyDNSServer> dns_server_;
};
/// @brief Check TKEY exchange fails for different reasons.
TEST_F(TKeyExchangeTest, exchangeFailure) {
string key_name("1234.sig-blu.example.nil.");
DnsServerPtr server(new DnsServer("foo", {}, IOAddress(TEST_ADDRESS),
TEST_PORT));
server->setServerPrincipal("DNS/blu.example.nil@EXAMPLE.NIL");
server->setKeyProto(IOFetch::Protocol::UDP);
setKeytab();
setAdministratorCCache();
{
SCOPED_TRACE("timeout when TKEY server is missing");
TKeyExchangeTestCallback callback(io_service_, TKeyExchange::TIMEOUT);
GssTsigKeyPtr key(new GssTsigKey(key_name));
ASSERT_EQ(key->getSecCtx().get(), GSS_C_NO_CONTEXT);
TKeyExchangePtr ex;
ASSERT_NO_THROW(ex.reset(new TKeyExchange(io_service_, server, key,
&callback, 100)));
ASSERT_NO_THROW(ex->doExchange());
io_service_->run();
io_service_->stopAndPoll();
StatMap stats = {
{ "tkey-sent", 1 },
{ "tkey-success", 0 },
{ "tkey-timeout", 1 },
{ "tkey-error", 0 }
};
checkStats(server, stats);
ASSERT_NE(key->getSecCtx().get(), GSS_C_NO_CONTEXT);
}
{
SCOPED_TRACE("stop when exchange is canceled");
TKeyExchangeTestCallback callback(io_service_, TKeyExchange::IO_STOPPED);
GssTsigKeyPtr key(new GssTsigKey(key_name));
ASSERT_EQ(key->getSecCtx().get(), GSS_C_NO_CONTEXT);
TKeyExchangePtr ex;
ASSERT_NO_THROW(ex.reset(new TKeyExchange(io_service_, server, key,
&callback, 100)));
ASSERT_NO_THROW(ex->doExchange());
ASSERT_NO_THROW(ex->cancel());
io_service_->run();
io_service_->stopAndPoll();
StatMap stats = {
{ "tkey-sent", 2 },
{ "tkey-success", 0 },
{ "tkey-timeout", 1 },
{ "tkey-error", 1 }
};
checkStats(server, stats);
ASSERT_NE(key->getSecCtx().get(), GSS_C_NO_CONTEXT);
}
{
SCOPED_TRACE("validation failure when TKEY response is not signed");
TKeyExchangeTestCallback callback(io_service_, TKeyExchange::UNSIGNED_RESPONSE);
GssTsigKeyPtr key(new GssTsigKey(key_name));
ASSERT_EQ(key->getSecCtx().get(), GSS_C_NO_CONTEXT);
TKeyExchangePtr ex;
ASSERT_NO_THROW(ex.reset(new TKeyExchange(io_service_, server, key,
&callback, 100)));
dns_server_.reset(new DummyDNSServer(io_service_, false, false, true));
dns_server_->start();
// The socket is now ready to receive the data. Let's post some request
// message then. Set timeout to some reasonable value to make sure that
// there is sufficient amount of time for the test to generate a
// response.
ASSERT_NO_THROW(ex->doExchange());
io_service_->run();
io_service_->stopAndPoll();
StatMap stats = {
{ "tkey-sent", 3 },
{ "tkey-success", 0 },
{ "tkey-timeout", 1 },
{ "tkey-error", 2 }
};
checkStats(server, stats);
ASSERT_NE(key->getSecCtx().get(), GSS_C_NO_CONTEXT);
}
}
/// @brief Check TKEY exchange fails when using Null IOService.
TEST_F(TKeyExchangeTest, exchangeBadUseNullIOService) {
string key_name("1234.sig-blu.example.nil.");
DnsServerPtr server(new DnsServer("foo", {}, IOAddress(TEST_ADDRESS),
TEST_PORT));
server->setServerPrincipal("DNS/blu.example.nil@EXAMPLE.NIL");
server->setKeyProto(IOFetch::Protocol::UDP);
setKeytab();
setAdministratorCCache();
TKeyExchangeTestCallback callback(IOServicePtr(), TKeyExchange::OTHER);
GssTsigKeyPtr key(new GssTsigKey(key_name));
ASSERT_EQ(key->getSecCtx().get(), GSS_C_NO_CONTEXT);
TKeyExchangePtr ex;
ASSERT_THROW(ex.reset(new TKeyExchange(IOServicePtr(), server, key,
&callback, 100)), BadValue);
StatMap stats = {
{ "tkey-sent", 0 },
{ "tkey-success", 0 },
{ "tkey-timeout", 0 },
{ "tkey-error", 0 }
};
checkStats(server, stats);
ASSERT_EQ(key->getSecCtx().get(), GSS_C_NO_CONTEXT);
}
/// @brief Check TKEY exchange fails when using same key name.
TEST_F(TKeyExchangeTest, exchangeBadUseSameKeyName) {
string key_name("1234.sig-blu.example.nil.");
DnsServerPtr server(new DnsServer("foo", {}, IOAddress(TEST_ADDRESS),
TEST_PORT));
server->setServerPrincipal("DNS/blu.example.nil@EXAMPLE.NIL");
server->setKeyProto(IOFetch::Protocol::UDP);
setKeytab();
setAdministratorCCache();
TKeyExchangeTestCallback callback(io_service_, TKeyExchange::SUCCESS, true);
TKeyExchangeTestCallback callback_bad(io_service_, TKeyExchange::INVALID_RESPONSE);
GssTsigKeyPtr key(new GssTsigKey(key_name));
GssTsigKeyPtr key_bad(new GssTsigKey(key_name));
ASSERT_EQ(key->getSecCtx().get(), GSS_C_NO_CONTEXT);
ASSERT_EQ(key_bad->getSecCtx().get(), GSS_C_NO_CONTEXT);
TKeyExchangePtr ex;
ASSERT_NO_THROW(ex.reset(new TKeyExchange(io_service_, server, key,
&callback, 100)));
TKeyExchangePtr ex_bad;
ASSERT_NO_THROW(ex_bad.reset(new TKeyExchange(io_service_, server, key_bad,
&callback_bad, 100)));
dns_server_.reset(new DummyDNSServer(io_service_, true, true, true));
dns_server_->start();
// The socket is now ready to receive the data. Let's post some request
// message then. Set timeout to some reasonable value to make sure that
// there is sufficient amount of time for the test to generate a
// response.
ASSERT_NO_THROW(ex->doExchange());
ASSERT_NO_THROW(ex_bad->doExchange());
io_service_->run();
StatMap stats = {
{ "tkey-sent", 2 },
{ "tkey-success", 1 },
{ "tkey-timeout", 0 },
{ "tkey-error", 1 }
};
checkStats(server, stats);
ASSERT_NE(key->getSecCtx().get(), GSS_C_NO_CONTEXT);
ASSERT_NE(key_bad->getSecCtx().get(), GSS_C_NO_CONTEXT);
ASSERT_NE(key->getSecCtx().get(), key_bad->getSecCtx().get());
}
/// @brief Check TKEY exchange fails when using same key.
TEST_F(TKeyExchangeTest, exchangeBadUseSameKey) {
string key_name("1234.sig-blu.example.nil.");
DnsServerPtr server(new DnsServer("foo", {}, IOAddress(TEST_ADDRESS),
TEST_PORT));
server->setServerPrincipal("DNS/blu.example.nil@EXAMPLE.NIL");
server->setKeyProto(IOFetch::Protocol::UDP);
setKeytab();
setAdministratorCCache();
TKeyExchangeTestCallback callback(io_service_, TKeyExchange::SUCCESS);
GssTsigKeyPtr key(new GssTsigKey(key_name));
ASSERT_EQ(key->getSecCtx().get(), GSS_C_NO_CONTEXT);
TKeyExchangePtr ex;
ASSERT_NO_THROW(ex.reset(new TKeyExchange(io_service_, server, key,
&callback, 100)));
dns_server_.reset(new DummyDNSServer(io_service_));
dns_server_->start();
// The socket is now ready to receive the data. Let's post some request
// message then. Set timeout to some reasonable value to make sure that
// there is sufficient amount of time for the test to generate a
// response.
ASSERT_NO_THROW(ex->doExchange());
io_service_->run();
ASSERT_NE(key->getSecCtx().get(), GSS_C_NO_CONTEXT);
auto old_ctx = key->getSecCtx().get();
TKeyExchangePtr ex_bad;
ASSERT_THROW(ex_bad.reset(new TKeyExchange(io_service_, server, key,
&callback, 100)), BadValue);
StatMap stats = {
{ "tkey-sent", 1 },
{ "tkey-success", 1 },
{ "tkey-timeout", 0 },
{ "tkey-error", 0 }
};
checkStats(server, stats);
ASSERT_NE(key->getSecCtx().get(), GSS_C_NO_CONTEXT);
ASSERT_EQ(key->getSecCtx().get(), old_ctx);
}
/// @brief Check TKEY exchange fails when doing second exchange on same key.
TEST_F(TKeyExchangeTest, exchangeBadUseSecondExchange) {
string key_name("1234.sig-blu.example.nil.");
DnsServerPtr server(new DnsServer("foo", {}, IOAddress(TEST_ADDRESS),
TEST_PORT));
server->setServerPrincipal("DNS/blu.example.nil@EXAMPLE.NIL");
server->setKeyProto(IOFetch::Protocol::UDP);
setKeytab();
setAdministratorCCache();
TKeyExchangeTestCallback callback(io_service_, TKeyExchange::SUCCESS);
GssTsigKeyPtr key(new GssTsigKey(key_name));
ASSERT_EQ(key->getSecCtx().get(), GSS_C_NO_CONTEXT);
TKeyExchangePtr ex;
ASSERT_NO_THROW(ex.reset(new TKeyExchange(io_service_, server, key,
&callback, 100)));
dns_server_.reset(new DummyDNSServer(io_service_));
dns_server_->start();
// The socket is now ready to receive the data. Let's post some request
// message then. Set timeout to some reasonable value to make sure that
// there is sufficient amount of time for the test to generate a
// response.
ASSERT_NO_THROW(ex->doExchange());
io_service_->run();
ASSERT_NE(key->getSecCtx().get(), GSS_C_NO_CONTEXT);
auto old_ctx = key->getSecCtx().get();
ASSERT_THROW(ex->doExchange(), InvalidOperation);
StatMap stats = {
{ "tkey-sent", 1 },
{ "tkey-success", 1 },
{ "tkey-timeout", 0 },
{ "tkey-error", 0 }
};
checkStats(server, stats);
ASSERT_NE(key->getSecCtx().get(), GSS_C_NO_CONTEXT);
ASSERT_EQ(key->getSecCtx().get(), old_ctx);
}
/// @brief Check TKEY exchange does not start with bad client credentials.
TEST_F(TKeyExchangeTest, badCredentials) {
string key_name("1234.sig-blu.example.nil.");
DnsServerPtr server(new DnsServer("foo", {}, IOAddress(TEST_ADDRESS),
TEST_PORT));
server->setClientPrincipal("DHCP/foo.bar@FOO.BAR");
server->setServerPrincipal("DNS/blu.example.nil@EXAMPLE.NIL");
server->setKeyProto(IOFetch::Protocol::UDP);
setKeytab();
setAdministratorCCache();
TKeyExchangeTestCallback callback(io_service_, TKeyExchange::BAD_CREDENTIALS);
GssTsigKeyPtr key(new GssTsigKey(key_name));
ASSERT_EQ(key->getSecCtx().get(), GSS_C_NO_CONTEXT);
TKeyExchangePtr ex;
ASSERT_NO_THROW(ex.reset(new TKeyExchange(io_service_, server, key,
&callback, 100)));
ASSERT_NO_THROW(ex->doExchange());
StatMap stats = {
{ "tkey-sent", 0 },
{ "tkey-success", 0 },
{ "tkey-timeout", 0 },
{ "tkey-error", 1 }
};
checkStats(server, stats);
EXPECT_EQ(key->getSecCtx().get(), GSS_C_NO_CONTEXT);
}
/// @brief Check TKEY exchange does not start with no server principal.
TEST_F(TKeyExchangeTest, noServerPrincipal) {
string key_name("1234.sig-blu.example.nil.");
DnsServerPtr server(new DnsServer("foo", {}, IOAddress(TEST_ADDRESS),
TEST_PORT));
// This could give a different error but in all cases an error.
server->setServerPrincipal("DNS/bad.example.nil@EXAMPLE.NIL");
server->setKeyProto(IOFetch::Protocol::UDP);
setKeytab();
setAdministratorCCache();
TKeyExchangeTestCallback callback(io_service_, TKeyExchange::OTHER);
GssTsigKeyPtr key(new GssTsigKey(key_name));
ASSERT_EQ(key->getSecCtx().get(), GSS_C_NO_CONTEXT);
TKeyExchangePtr ex;
ASSERT_NO_THROW(ex.reset(new TKeyExchange(io_service_, server, key,
&callback, 100)));
ASSERT_NO_THROW(ex->doExchange());
StatMap stats = {
{ "tkey-sent", 0 },
{ "tkey-success", 0 },
{ "tkey-timeout", 0 },
{ "tkey-error", 1 }
};
checkStats(server, stats);
EXPECT_EQ(key->getSecCtx().get(), GSS_C_NO_CONTEXT);
}
/// @brief Check TKEY exchange fails with too short client credential lifetime.
TEST_F(TKeyExchangeTest, tooShortLifetime) {
string key_name("1234.sig-blu.example.nil.");
DnsServerPtr server(new DnsServer("foo", {}, IOAddress(TEST_ADDRESS),
TEST_PORT));
server->setKeyLifetime(0x7fff0000);
server->setServerPrincipal("DNS/blu.example.nil@EXAMPLE.NIL");
server->setKeyProto(IOFetch::Protocol::UDP);
setKeytab();
setAdministratorCCache();
TKeyExchangeTestCallback callback(io_service_, TKeyExchange::BAD_CREDENTIALS);
GssTsigKeyPtr key(new GssTsigKey(key_name));
ASSERT_EQ(key->getSecCtx().get(), GSS_C_NO_CONTEXT);
TKeyExchangePtr ex;
ASSERT_NO_THROW(ex.reset(new TKeyExchange(io_service_, server, key,
&callback, 100)));
dns_server_.reset(new DummyDNSServer(io_service_));
dns_server_->start();
// The socket is now ready to receive the data. Let's post some request
// message then. Set timeout to some reasonable value to make sure that
// there is sufficient amount of time for the test to generate a
// response.
ASSERT_NO_THROW(ex->doExchange());
io_service_->run();
StatMap stats = {
{ "tkey-sent", 1 },
{ "tkey-success", 0 },
{ "tkey-timeout", 0 },
{ "tkey-error", 1 }
};
checkStats(server, stats);
EXPECT_NE(key->getSecCtx().get(), GSS_C_NO_CONTEXT);
}
/// @brief Check TKEY exchange succeeds.
TEST_F(TKeyExchangeTest, exchangeSuccess) {
string key_name("1234.sig-blu.example.nil.");
GssTsigKeyPtr key(new GssTsigKey(key_name));
DnsServerPtr server(new DnsServer("foo", {}, IOAddress(TEST_ADDRESS),
TEST_PORT));
server->setServerPrincipal("DNS/blu.example.nil@EXAMPLE.NIL");
server->setKeyProto(IOFetch::Protocol::UDP);
setKeytab();
setAdministratorCCache();
TKeyExchangeTestCallback callback(io_service_, TKeyExchange::SUCCESS);
TKeyExchangePtr ex;
ASSERT_NO_THROW(ex.reset(new TKeyExchange(io_service_, server, key,
&callback, 100)));
dns_server_.reset(new DummyDNSServer(io_service_));
dns_server_->start();
// The socket is now ready to receive the data. Let's post some request
// message then. Set timeout to some reasonable value to make sure that
// there is sufficient amount of time for the test to generate a
// response.
// This exchange should succeed.
ASSERT_NO_THROW(ex->doExchange());
io_service_->run();
StatMap stats = {
{ "tkey-sent", 1 },
{ "tkey-success", 1 },
{ "tkey-timeout", 0 },
{ "tkey-error", 0 }
};
checkStats(server, stats);
EXPECT_NE(key->getSecCtx().get(), GSS_C_NO_CONTEXT);
}
}

View File

@ -0,0 +1,176 @@
// Copyright (C) 2021-2024 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 <dns/message.h>
#include <dns/messagerenderer.h>
#include <dns/name.h>
#include <dns/opcode.h>
#include <dns/rcode.h>
#include <dns/rrclass.h>
#include <dns/rrset.h>
#include <dns/time_utils.h>
#include <dns/tsig.h>
#include <gss_tsig_api.h>
#include <gss_tsig_api_utils.h>
#include <testutils/gtest_utils.h>
#include <gtest/gtest.h>
using namespace std;
using namespace isc;
using namespace isc::dns;
using namespace isc::dns::rdata;
using namespace isc::dns::rdata::generic;
using namespace isc::gss_tsig;
using namespace isc::gss_tsig::test;
using namespace isc::util;
namespace {
/// @brief Test fixture for testing the GSS-API with Kerberos 5.
class TKeyTest : public GssApiBaseTest {
public:
/// @brief Constructor.
TKeyTest() : GssApiBaseTest(), msg_() {
}
/// @brief Build a TKEY message.
///
/// @param query True if query, false if response.
/// @param qid Message ID.
/// @param key_name DNS name of the key.
/// @param token GSS-API token.
/// @return The output buffer with the TKEY message.
OutputBufferPtr buildTKey(bool query, qid_t qid, const Name& key_name,
GssApiBuffer& token);
/// @brief Decode a TKEY message.
///
/// @param input Input buffer.
/// @param[out] query True if query, false if response.
/// @param[out] qid Message ID.
/// @param[out] key_name DNS name of the key.
/// @param[out] token Pointer to GSS-API token.
void decodeTKey(const vector<uint8_t>& input, bool& query, qid_t& qid,
Name& key_name, GssApiBufferPtr& token);
/// @brief Pointer to the current message.
MessagePtr msg_;
};
OutputBufferPtr
TKeyTest::buildTKey(bool query, qid_t qid, const Name& key_name,
GssApiBuffer& token) {
msg_.reset(new Message(Message::RENDER));
msg_->setHeaderFlag(Message::HEADERFLAG_QR, query);
msg_->setQid(qid);
msg_->setRcode(Rcode::NOERROR());
msg_->setOpcode(Opcode(Opcode::QUERY_CODE));
Question question(key_name, RRClass::ANY(), RRType::TKEY());
msg_->addQuestion(question);
RRsetPtr rrset(new RRset(key_name, RRClass::ANY(), RRType::TKEY(),
RRTTL(0)));
Name algorithm("gss-tsig.");
uint32_t inception = timeFromText32("20210501120000");
uint32_t expire = timeFromText32("20210501130000");
uint16_t mode = TKEY::GSS_API_MODE;
uint16_t error = Rcode::NOERROR().getCode();
uint16_t key_len = static_cast<uint16_t>(token.getLength());
ConstRdataPtr rdata(new TKEY(algorithm, inception, expire,
mode, error, key_len,
token.getValue(), 0, 0));
rrset->addRdata(rdata);
msg_->addRRset(Message::SECTION_ADDITIONAL, rrset);
MessageRenderer renderer;
OutputBufferPtr buf(new OutputBuffer(4096));
renderer.setBuffer(buf.get());
EXPECT_NO_THROW(msg_->toWire(renderer));
return (buf);
}
void
TKeyTest::decodeTKey(const vector<uint8_t>& input, bool& query, qid_t& qid,
Name& key_name, GssApiBufferPtr& token) {
msg_.reset(new Message(Message::PARSE));
ASSERT_FALSE(input.empty());
InputBuffer buf(&input[0], input.size());
EXPECT_NO_THROW(msg_->fromWire(buf));
query = msg_->getHeaderFlag(Message::HEADERFLAG_QR);
qid = msg_->getQid();
EXPECT_EQ(Rcode::NOERROR(), msg_->getRcode());
EXPECT_EQ(Opcode(Opcode::QUERY_CODE), msg_->getOpcode());
ASSERT_EQ(1, msg_->getRRCount(Message::SECTION_QUESTION));
QuestionPtr question = *msg_->beginQuestion();
ASSERT_TRUE(question);
key_name = question->getName();
EXPECT_EQ(RRClass::ANY(), question->getClass());
EXPECT_EQ(RRType::TKEY(), question->getType());
EXPECT_EQ(0, msg_->getRRCount(Message::SECTION_ANSWER));
EXPECT_EQ(0, msg_->getRRCount(Message::SECTION_AUTHORITY));
ASSERT_EQ(1, msg_->getRRCount(Message::SECTION_ADDITIONAL));
RRsetPtr rrset = *msg_->beginSection(Message::SECTION_ADDITIONAL);
ASSERT_TRUE(rrset);
EXPECT_EQ(key_name, rrset->getName());
EXPECT_EQ(RRClass::ANY(), rrset->getClass());
ASSERT_EQ(RRType::TKEY(), rrset->getType());
EXPECT_EQ(RRTTL(0), rrset->getTTL());
ASSERT_EQ(1, rrset->getRdataCount());
auto rdata_it = rrset->getRdataIterator();
try {
const TKEY& tkey =
dynamic_cast<const TKEY&>(rdata_it->getCurrent());
EXPECT_EQ(Name("gss-tsig."), tkey.getAlgorithm());
EXPECT_EQ("20210501120000", tkey.getInceptionDate());
EXPECT_EQ("20210501130000", tkey.getExpireDate());
EXPECT_EQ(TKEY::GSS_API_MODE, tkey.getMode());
EXPECT_EQ(Rcode::NOERROR().getCode(), tkey.getError());
token.reset(new GssApiBuffer(tkey.getKeyLen(), tkey.getKey()));
EXPECT_EQ(0, tkey.getOtherLen());
EXPECT_FALSE(tkey.getOtherData());
} catch (const exception& ex) {
FAIL() << ex.what();
}
}
/// @brief Check TKEY message build.
TEST_F(TKeyTest, build) {
Name key_name("1234.sig-blu.example.nil.");
qid_t qid = 0x4567;
GssApiBuffer token("foobar");
OutputBufferPtr output;
msg_.reset();
ASSERT_NO_THROW(output = buildTKey(true, qid, key_name, token));
ASSERT_TRUE(msg_);
ASSERT_NE(0, output->getLength());
}
/// @brief Check TKEY message decode.
TEST_F(TKeyTest, decode) {
Name key_name("1234.sig-blu.example.nil.");
qid_t qid = 0x4567;
GssApiBuffer token("foobar");
OutputBufferPtr output;
ASSERT_NO_THROW(output = buildTKey(true, qid, key_name, token));
ASSERT_NE(0, output->getLength());
const vector<uint8_t>& input = output->getVector();
bool query = false;
qid_t qid2 = 0;
Name key_name2("foo");
GssApiBufferPtr token2;
msg_.reset();
EXPECT_NO_THROW(decodeTKey(input, query, qid2, key_name2, token2));
ASSERT_TRUE(msg_);
EXPECT_TRUE(query);
EXPECT_EQ(qid, qid2);
EXPECT_EQ(key_name, key_name2);
ASSERT_TRUE(token2);
EXPECT_EQ("foobar", token2->getString());
}
}

View File

@ -0,0 +1,27 @@
SUBDIRS = .
AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
AM_CPPFLAGS += -I$(top_srcdir)/src/bin -I$(top_builddir)/src/bin
AM_CPPFLAGS += -I$(top_builddir)/src/hooks/d2/gss_tsig -I$(top_srcdir)/src/hooks/d2/gss_tsig
AM_CPPFLAGS += -DTEST_DATA_DIR=\"$(abs_srcdir)\"
AM_CPPFLAGS += $(GSSAPI_CFLAGS) $(BOOST_INCLUDES)
AM_CPPFLAGS += $(CRYPTO_CFLAGS) $(CRYPTO_INCLUDES)
AM_CXXFLAGS = $(KEA_CXXFLAGS)
CLEANFILES = *.gcno *.gcda
if HAVE_GTEST
noinst_LTLIBRARIES = libgsstsigtest.la
libgsstsigtest_la_SOURCES = gss_tsig_dns_server.cc gss_tsig_dns_server.h
libgsstsigtest_la_CXXFLAGS = $(AM_CXXFLAGS)
libgsstsigtest_la_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) $(LOG4CPLUS_INCLUDES)
libgsstsigtest_la_LIBADD = $(top_builddir)/src/lib/d2srv/libkea-d2srv.la
libgsstsigtest_la_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) $(GTEST_LDFLAGS)
endif

View File

@ -0,0 +1,265 @@
// Copyright (C) 2021-2022 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 <d2srv/dns_client.h>
#include <dns/messagerenderer.h>
#include <dns/opcode.h>
#include <dns/rdata.h>
#include <dns/rdataclass.h>
#include <gss_tsig_dns_server.h>
#include <gtest/gtest.h>
namespace isc {
namespace gss_tsig {
namespace test {
using namespace boost::asio;
using namespace boost::asio::ip;
using namespace isc;
using namespace isc::asiolink;
using namespace isc::d2;
using namespace isc::dns;
using namespace isc::dns::rdata;
using namespace isc::dns::rdata::generic;
using namespace isc::util;
using namespace std;
namespace ph = std::placeholders;
const char* DummyDNSServer::TEST_ADDRESS = "127.0.0.1";
void
DummyDNSServer::start() {
// In order to perform the full test, when the client sends the request
// and receives a response from the server, we have to emulate the
// server's response in the test. A request will be sent via loopback
// interface to 127.0.0.1 and known test port. Response must be sent
// to 127.0.0.1 and the source port which has been used to send the
// request. A new socket is created, specifically to handle sending
// responses. The reuse address option is set so as both sockets can
// use the same address. This new socket is bound to the test address
// and port, where requests will be sent.
socket_.reset(new udp::socket(io_service_->getInternalIOService(),
boost::asio::ip::udp::v4()));
socket_->set_option(socket_base::reuse_address(true));
socket_->bind(udp::endpoint(make_address(TEST_ADDRESS), TEST_PORT));
// Once socket is created, we can post an IO request to receive some
// packet from this socket. This is asynchronous operation and
// nothing is received until another IO request to send a query is
// posted and the run() is invoked on this IO. A callback function is
// attached to this asynchronous read. This callback function requires
// that a socket object used to receive the request is passed to it,
// because the same socket will be used by the callback function to send
// a response. Also, the remote object is passed to the callback,
// because it holds a source address and port where request originated.
// Callback function will send a response to this address and port.
// The last parameter holds a length of the received request. It is
// required to construct a response.
endpoint_.reset(new udp::endpoint());
socket_->async_receive_from(boost::asio::buffer(receive_buffer_,
sizeof(receive_buffer_)),
*endpoint_,
std::bind(&DummyDNSServer::udpTKeyExchangeReceiveHandler,
this, socket_.get(), endpoint_.get(),
ph::_2));
}
void
DummyDNSServer::readTKey(InputBufferPtr inbuf, GssApiBufferPtr& intoken,
Name& key_name, qid_t& qid) {
MessagePtr msg(new Message(Message::PARSE));
msg->fromWire(*inbuf);
qid = msg->getQid();
// Validate the TKEY response.
ASSERT_FALSE(msg->getHeaderFlag(Message::HEADERFLAG_QR));
ASSERT_EQ(msg->getRcode(), Rcode::NOERROR());
ASSERT_EQ(msg->getOpcode(), Opcode::QUERY());
ASSERT_EQ(msg->getRRCount(Message::SECTION_QUESTION), 1);
QuestionPtr question = *msg->beginQuestion();
ASSERT_TRUE(question);
ASSERT_EQ(question->getClass(), RRClass::ANY());
ASSERT_EQ(question->getType(), RRType::TKEY());
RRsetPtr rrset = *msg->beginSection(Message::SECTION_ADDITIONAL);
ASSERT_TRUE(rrset);
key_name = rrset->getName();
ASSERT_EQ(rrset->getClass(), RRClass::ANY());
ASSERT_EQ(rrset->getType(), RRType::TKEY());
ASSERT_EQ(rrset->getRdataCount(), 1);
auto rdata_it = rrset->getRdataIterator();
const TKEY& tkey = dynamic_cast<const TKEY&>(rdata_it->getCurrent());
ASSERT_EQ(tkey.getError(), Rcode::NOERROR_CODE);
intoken.reset(new GssApiBuffer(tkey.getKeyLen(), tkey.getKey()));
}
void
DummyDNSServer::writeTKey(GssApiBuffer& outtoken, Name& key_name,
OutputBufferPtr& outbuf, GssTsigKeyPtr srv_key,
qid_t qid, bool sign) {
MessagePtr msg(new Message(Message::RENDER));
msg->setOpcode(Opcode::QUERY());
msg->setRcode(Rcode::NOERROR());
msg->setQid(qid);
msg->setHeaderFlag(Message::HEADERFLAG_QR, true);
msg->addQuestion(Question(key_name, RRClass::ANY(), RRType::TKEY()));
// Create the TKEY Resource Record.
RRsetPtr tkey_rrset(new RRset(key_name, RRClass::ANY(),
RRType::TKEY(), RRTTL(0)));
Name algorithm("gss-tsig.");
uint32_t inception = static_cast<uint32_t>(time(0));
uint32_t expire = inception + 3600;
uint16_t mode = TKEY::GSS_API_MODE;
uint16_t error = Rcode::NOERROR().getCode();
uint16_t key_len = static_cast<uint16_t>(outtoken.getLength());
ConstRdataPtr tkey_rdata(new TKEY(algorithm, inception, expire, mode, error,
key_len, outtoken.getValue(), 0, 0));
tkey_rrset->addRdata(tkey_rdata);
msg->addRRset(Message::SECTION_ANSWER, tkey_rrset);
// Encode the TKEY request.
MessageRenderer renderer;
outbuf.reset(new OutputBuffer(MAX_SIZE));
renderer.setBuffer(outbuf.get());
renderer.setLengthLimit(MAX_SIZE);
if (sign && srv_key) {
TSIGContextPtr context = srv_key->createContext();
msg->toWire(renderer, context.get());
} else {
msg->toWire(renderer);
}
}
void
DummyDNSServer::writeTKeyDuplicate(OutputBufferPtr& outbuf, qid_t qid) {
MessagePtr msg(new Message(Message::RENDER));
msg->setOpcode(Opcode::QUERY());
// BADNAME Duplicate key name [RFC 2930]
msg->setRcode(Rcode(20));
msg->setQid(qid);
// Encode the TKEY request.
MessageRenderer renderer;
outbuf.reset(new OutputBuffer(MAX_SIZE));
renderer.setBuffer(outbuf.get());
renderer.setLengthLimit(MAX_SIZE);
msg->toWire(renderer);
}
void
DummyDNSServer::udpTKeyExchangeReceiveHandler(udp::socket* socket,
udp::endpoint* remote,
size_t receive_length) {
OM_uint32 lifetime = 0;
GssApiName srv_name("DNS/blu.example.nil@EXAMPLE.NIL");
GssApiCredPtr srv_cred(new GssApiCred(srv_name, GSS_C_ACCEPT, lifetime));
InputBufferPtr recv_buf(new InputBuffer(receive_buffer_, receive_length));
GssApiBufferPtr intoken;
qid_t qid;
Name key_name("foo");
readTKey(recv_buf, intoken, key_name, qid);
ASSERT_TRUE(intoken);
string key_str = key_name.toText();
if (match_exact_key_) {
ASSERT_EQ(key_str, "1234.sig-blu.example.nil.");
}
OutputBufferPtr send_buf;
if (keys_.find(key_str) != keys_.end()) {
ASSERT_NO_THROW(writeTKeyDuplicate(send_buf, qid));
} else {
keys_.insert(key_str);
srv_key_.reset(new ManagedKey(key_str));
GssApiBuffer outtoken;
GssApiName client_name;
ASSERT_NO_THROW(srv_key_->getSecCtx().accept(*srv_cred, *intoken,
client_name, outtoken));
ASSERT_NO_THROW(writeTKey(outtoken, key_name, send_buf, srv_key_, qid,
sign_tkey_));
}
ASSERT_TRUE(send_buf);
// A response message is now ready to send. Send it!
socket->send_to(boost::asio::buffer(send_buf->getData(),
send_buf->getLength()),
*remote);
if (!go_on_) {
return;
}
if (only_tkey_) {
socket_->async_receive_from(boost::asio::buffer(receive_buffer_,
sizeof(receive_buffer_)),
*endpoint_,
std::bind(&DummyDNSServer::udpTKeyExchangeReceiveHandler,
this, socket_.get(), endpoint_.get(),
ph::_2));
} else {
socket_->async_receive_from(boost::asio::buffer(receive_buffer_,
sizeof(receive_buffer_)),
*endpoint_,
std::bind(&DummyDNSServer::udpDNSUpdateReceiveHandler,
this, socket_.get(), endpoint_.get(),
ph::_2));
}
}
void
DummyDNSServer::udpDNSUpdateReceiveHandler(udp::socket* socket,
udp::endpoint* remote,
size_t receive_length) {
TSIGContextPtr context;
if (sign_update_ && srv_key_) {
context = srv_key_->createContext();
}
isc::util::InputBuffer received_data_buffer(receive_buffer_,
receive_length);
dns::Message request(Message::PARSE);
request.fromWire(received_data_buffer);
// If context is not NULL, then we need to verify the message.
if (context) {
TSIGError error = context->verify(request.getTSIGRecord(),
receive_buffer_, receive_length);
if (error != TSIGError::NOERROR()) {
isc_throw(TSIGVerifyError, "TSIG verification failed: "
<< error.toText());
}
}
dns::Message response(Message::RENDER);
response.setOpcode(Opcode(Opcode::UPDATE_CODE));
response.setHeaderFlag(dns::Message::HEADERFLAG_QR, true);
response.setQid(request.getQid());
response.setRcode(Rcode::NOERROR());
dns::Question question(Name("example.com."),
RRClass::IN(), RRType::SOA());
response.addQuestion(question);
MessageRenderer renderer;
if (context) {
response.toWire(renderer, context.get());
} else {
response.toWire(renderer);
}
// A response message is now ready to send. Send it!
socket->send_to(boost::asio::buffer(renderer.getData(),
renderer.getLength()), *remote);
}
}
}
}

View File

@ -0,0 +1,170 @@
// Copyright (C) 2021-2022 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 GSS_TSIG_DNS_SERVER_H
#define GSS_TSIG_DNS_SERVER_H
#include <asiolink/io_service.h>
#include <dns/message.h>
#include <gss_tsig_api.h>
#include <gss_tsig_context.h>
#include <managed_key.h>
#include <tkey_exchange.h>
#include <util/buffer.h>
#include <boost/asio/ip/udp.hpp>
#include <boost/asio/socket_base.hpp>
namespace isc {
namespace gss_tsig {
namespace test {
class DummyDNSServer {
public:
/// @brief Constructor.
///
/// @param io_service The IOService which handles IO operations.
/// @param sign_tkey The flag which specifies if the server should sign the
/// TKEY response.
/// @param sign_update The flag which specifies if the server should sign
/// the DNS update.
/// @param only_tkey The flag which specifies if the server should only do
/// TKEY exchanges.
/// @param go_on The flag which specifies if the server should continue with
/// receiving DNS updates.
/// @param match_exact_key The flag which specifies if the server should
/// check if the key name matches expected value.
DummyDNSServer(isc::asiolink::IOServicePtr io_service,
bool sign_tkey = true, bool sign_update = true,
bool only_tkey = false, bool go_on = true,
bool match_exact_key = true) :
io_service_(io_service), sign_tkey_(sign_tkey),
sign_update_(sign_update), only_tkey_(only_tkey), go_on_(go_on),
match_exact_key_(match_exact_key) {
}
/// @brief Destructor.
~DummyDNSServer() = default;
/// @brief Start function which starts listening on UDP endpoint.
///
/// @param io_service The IOService which handles IO operations.
void start();
/// @brief Read TKEY request from buffer.
///
/// @param inbuf The request buffer.
/// @param intoken The TKEY data which is extracted.
/// @param key_name The key name.
/// @param qid The request message qid.
void readTKey(isc::util::InputBufferPtr inbuf,
isc::gss_tsig::GssApiBufferPtr& intoken,
isc::dns::Name& key_name, isc::dns::qid_t& qid);
/// @brief Write TKEY reply to buffer.
///
/// @param outtoken The TKEY data.
/// @param key_name The key name.
/// @param outbuf The reply buffer.
/// @param srv_key The server key.
/// @param qid The reply message qid.
/// @param sign The flag which specifies if the server should sign the
/// response.
void writeTKey(isc::gss_tsig::GssApiBuffer& outtoken,
isc::dns::Name& key_name,
isc::util::OutputBufferPtr& outbuf,
isc::gss_tsig::GssTsigKeyPtr srv_key,
isc::dns::qid_t qid, bool sign);
/// @brief Write BADNAME (20) TKEY reply to buffer in case of duplicate key.
///
/// @param outbuf The reply buffer.
/// @param qid The reply message qid.
void writeTKeyDuplicate(isc::util::OutputBufferPtr& outbuf,
isc::dns::qid_t qid);
/// @brief Handler invoked when test TKEY request is received.
///
/// This callback handler is installed when performing async read on a
/// socket to emulate reception of the TKEY request by a server.
/// As a result, this handler will send an appropriate TKEY response
/// message back to the address from which the request has come.
///
/// @param socket A pointer to a socket used to receive a query and send a
/// response.
/// @param remote A pointer to an object which specifies the host (address
/// and port) from which a request has come.
/// @param receive_length A length (in bytes) of the received data.
void udpTKeyExchangeReceiveHandler(boost::asio::ip::udp::socket* socket,
boost::asio::ip::udp::endpoint* remote,
size_t receive_length);
/// @brief Handler invoked when test DNS update is received.
///
/// This callback handler is installed when performing async read on a
/// socket to emulate reception of the DNS Update request by a server.
/// As a result, this handler will send an appropriate DNS Update response
/// message back to the address from which the request has come.
///
/// @param socket A pointer to a socket used to receive a query and send a
/// response.
/// @param remote A pointer to an object which specifies the host (address
/// and port) from which a request has come.
/// @param receive_length A length (in bytes) of the received data.
void udpDNSUpdateReceiveHandler(boost::asio::ip::udp::socket* socket,
boost::asio::ip::udp::endpoint* remote,
size_t receive_length);
/// @brief The IOService which handles IO operations.
isc::asiolink::IOServicePtr io_service_;
/// @brief The flag which specifies if the TKEY response should be signed.
bool sign_tkey_;
/// @brief The flag which specifies if the DNS update should be signed.
bool sign_update_;
/// @brief The flag which specifies if the server should only do TKEY
/// exchanges.
bool only_tkey_;
/// @brief The flag which specifies if the server should continue with
/// receiving DNS updates.
bool go_on_;
/// @brief The flag which specifies if the server should check if the key
/// name matches expected value.
bool match_exact_key_;
/// @brief Maximum size of the internal buffers.
static const size_t MAX_SIZE = 4096;
/// @brief Server listening address.
static const char* TEST_ADDRESS;
/// @brief Server listening port.
static const uint16_t TEST_PORT = 5376;
/// @brief The receive buffer.
uint8_t receive_buffer_[MAX_SIZE];
/// @brief The UDP socket.
std::unique_ptr<boost::asio::ip::udp::socket> socket_;
/// @brief The UDP socket endpoint.
std::unique_ptr<boost::asio::ip::udp::endpoint> endpoint_;
/// @brief The GSS-TSIG server key.
ManagedKeyPtr srv_key_;
/// @brief The list of sent keys.
std::set<std::string> keys_;
};
}
}
}
#endif

View File

@ -0,0 +1,12 @@
if not gtest.found()
subdir_done()
endif
current_source_dir = meson.current_source_dir()
ddns_gss_tsig_testutils_lib = static_library(
'ddns-gss-tsig-testutils',
'gss_tsig_dns_server.cc',
cpp_args: [f'-DTEST_DATA_DIR="@current_source_dir@"'],
dependencies: [gtest, crypto],
include_directories: [include_directories('.'), include_directories('..')] + INCLUDES,
)

View File

@ -0,0 +1,582 @@
// Copyright (C) 2021-2024 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 <d2srv/d2_log.h>
#include <dns/messagerenderer.h>
#include <dns/opcode.h>
#include <dns/rdataclass.h>
#include <stats/stats_mgr.h>
#include <gss_tsig_context.h>
#include <gss_tsig_key.h>
#include <gss_tsig_log.h>
#include <tkey_exchange.h>
#include <limits>
#include <sstream>
namespace isc {
namespace gss_tsig {
namespace {
// OutputBuffer objects are pre-allocated before data is written to them.
// This is a default number of bytes for the buffers we create within
// TKeyExchange class.
const size_t DEFAULT_BUFFER_SIZE = 4096;
}
using namespace isc::asiolink;
using namespace isc::asiodns;
using namespace isc::dns;
using namespace isc::dns::rdata;
using namespace isc::dns::rdata::generic;
using namespace isc::log;
using namespace isc::stats;
using namespace isc::util;
using namespace std;
string
TKeyExchange::statusToText(Status status) {
switch (status) {
case SUCCESS:
return ("response received and is ok");
case TIMEOUT:
return ("no response, timeout");
case IO_STOPPED:
return ("IO was stopped");
case INVALID_RESPONSE:
return ("response received but invalid");
case UNSIGNED_RESPONSE:
return ("response received but not signed");
case BAD_CREDENTIALS:
return ("bad client credentials");
default:
return ("other, unclassified error");
}
}
// This class provides the implementation for the TKeyExchange. This allows for
// the separation of the TKeyExchange interface from the implementation details.
// Currently, implementation uses IOFetch object to handle asynchronous
// communication with the DNS. This design may be revisited in the future. If
// implementation is changed, the TKeyExchange API will remain unchanged thanks
// to this separation.
class TKeyExchangeImpl : public IOFetch::Callback {
public:
/// @brief The TKEY exchange state.
enum State {
NONE, ///< Initial state: no action has been initiated.
STARTED, ///< The TKEY exchange has been started.
STOPPED, ///< The TKEY exchange has been canceled.
SUCCESS, ///< The TKEY exchange has succeeded.
FAILURE, ///< The TKEY exchange has failed.
};
/// @brief Constructor.
///
/// @param io_service The IOService which handles IO operations.
/// @param server The server for which the TKEY exchange is performed.
/// @param key The TKEY being updated.
/// @param callback Pointer to an object implementing @c TKeyExchange::Callback
/// class. This object will be called when DNS message exchange completes or
/// if an error occurs. NULL value disables callback invocation.
/// @param timeout The timeout for the IO operations.
/// @param flags The flags used for the TKEY exchange.
/// @throw BadValue if io_service is null.
/// @throw BadValue if key's security context has already been used.
TKeyExchangeImpl(const IOServicePtr& io_service, const DnsServerPtr& server,
const GssTsigKeyPtr& key, TKeyExchange::Callback* callback,
uint32_t timeout, OM_uint32 flags);
/// @brief Destructor.
virtual ~TKeyExchangeImpl();
/// @brief This internal callback is called when the DNS update message
/// exchange is complete. It further invokes the external callback provided
/// by a caller.
///
/// @param result The result of the IOFetch operation.
virtual void operator()(IOFetch::Result result);
/// @brief This function handles the repeated communication with the DNS
/// server trying to complete the TKEY exchange.
void doExchange();
/// @brief This function cancels the started TKEY exchange.
void cancel();
/// @brief Gets IO service.
///
/// @return IOService object, used for all ASIO operations.
isc::asiolink::IOServicePtr getIOService() {
return (io_service_);
}
/// @brief Sets IO service.
///
/// @param io_service IOService object, used for all ASIO operations.
void setIOService(const isc::asiolink::IOServicePtr& io_service) {
io_service_ = io_service;
}
private:
/// @brief Internal exchange function which handles all remaining steps of
/// the key exchange.
///
/// @Note If an error occurs, or the exchange succeeds, the internal status
/// is updated and the user callback is called with appropriate value.
///
/// @param intoken The buffer used to continue the key exchange.
void doExchangeInternal(GssApiBufferPtr intoken);
/// @brief Call the user callback with the current internal status.
///
/// @param status The status of the TKEY exchange.
void callCallback(TKeyExchange::Status status);
/// @brief Acquire credentials.
void acquireCredentials();
/// @brief Read the TKEY received from the DNS server.
///
/// @param outbuf The buffer used to read the TKEY value.
/// @return The buffer containing the TKEY value.
GssApiBufferPtr readTKey(const OutputBufferPtr& outbuf);
/// @brief Verify the received TKEY.
///
/// @return true if the TKEY passes validation, false otherwise.
bool verifyTKey();
/// @brief Create DNS request for the TKEY.
///
/// @param outtoken The buffer containing the TKEY used to create the DNS
/// request.
void createTKeyRequest(const GssApiBufferPtr& outtoken);
/// @brief Update statistics.
///
/// @param stat The statistics name.
void incrStats(const string& stat);
/// @brief The IOService which handles IO operations.
IOServicePtr io_service_;
/// @brief The state of the TKEY exchange.
State state_;
/// @brief A buffer holding response from a DNS.
OutputBufferPtr in_buf_;
/// @brief A buffer holding request for a DNS.
OutputBufferPtr out_buf_;
/// @brief A caller-supplied external callback which is invoked when DNS
/// message exchange is complete or interrupted.
TKeyExchange::Callback* callback_;
/// @brief The DNS server which requires TKEY exchange.
DnsServerPtr server_;
/// @brief The GSS-TSIG key.
GssTsigKeyPtr key_;
/// @brief The GSS-TSIG flags used for the TKEY exchange.
OM_uint32 flags_;
/// @brief The credentials.
GssApiCredPtr cred_;
/// @brief The DNS message used to communicate with the DNS server.
MessagePtr msg_;
/// @brief The timeout for the IO operations.
uint32_t timeout_;
/// @brief The IOFetch used to do the TKEY exchange.
IOFetchPtr io_fetch_;
};
TKeyExchangeImpl::TKeyExchangeImpl(const IOServicePtr& io_service,
const DnsServerPtr& server,
const GssTsigKeyPtr& key,
TKeyExchange::Callback* callback,
uint32_t timeout, OM_uint32 flags)
: io_service_(io_service), state_(NONE), in_buf_(), out_buf_(),
callback_(callback), server_(server), key_(key), flags_(flags), cred_(),
msg_(), timeout_(timeout) {
if (!io_service) {
isc_throw(BadValue, "null IOService");
}
if (key->getSecCtx().get() != GSS_C_NO_CONTEXT) {
isc_throw(BadValue, "wrong security context state");
}
}
TKeyExchangeImpl::~TKeyExchangeImpl() {
cancel();
}
void
TKeyExchangeImpl::operator()(IOFetch::Result result) {
// Get the status from IO. If no success, we just call user's callback
// and pass the status code.
GssApiBufferPtr intoken;
switch (result) {
case IOFetch::SUCCESS:
intoken = readTKey(out_buf_);
if (!intoken || intoken->empty()) {
LOG_ERROR(gss_tsig_logger, TKEY_EXCHANGE_FAIL_EMPTY_IN_TOKEN);
incrStats("tkey-error");
callCallback(TKeyExchange::INVALID_RESPONSE);
} else {
// The TKEY payload from the server response is used for
// the GSS-API security context establishment.
doExchangeInternal(intoken);
}
return;
case IOFetch::TIME_OUT:
LOG_ERROR(gss_tsig_logger, TKEY_EXCHANGE_FAIL_IO_TIMEOUT);
incrStats("tkey-timeout");
callCallback(TKeyExchange::TIMEOUT);
return;
case IOFetch::STOPPED:
LOG_ERROR(gss_tsig_logger, TKEY_EXCHANGE_FAIL_IO_STOPPED);
incrStats("tkey-error");
callCallback(TKeyExchange::IO_STOPPED);
return;
default:
LOG_ERROR(gss_tsig_logger, TKEY_EXCHANGE_FAIL_IO_ERROR)
.arg(result);
incrStats("tkey-error");
callCallback(TKeyExchange::OTHER);
return;
}
}
void
TKeyExchangeImpl::acquireCredentials() {
const string& cred_princ = server_->getClientPrincipal();
if (cred_princ.empty()) {
return;
}
OM_uint32 lifetime(0);
GssApiName cname(cred_princ);
cred_.reset(new GssApiCred(cname, GSS_C_INITIATE, lifetime));
if (lifetime == 0) {
isc_throw(GssCredExpired, "credentials expired for " << cred_princ);
}
}
bool
TKeyExchangeImpl::verifyTKey() {
// The last TKEY response must be signed.
const TSIGRecord* tsig = msg_->getTSIGRecord();
if (!tsig) {
LOG_ERROR(gss_tsig_logger, TKEY_EXCHANGE_FAIL_NOT_SIGNED);
return (false);
}
GssTsigContextPtr tkey_ctx(new GssTsigContext(*key_));
tkey_ctx->setState(TSIGContext::SENT_REQUEST);
TSIGError error = tkey_ctx->verify(tsig, out_buf_->getData(),
out_buf_->getLength());
if (error != TSIGError::NOERROR()) {
LOG_ERROR(gss_tsig_logger, TKEY_EXCHANGE_FAILED_TO_VERIFY);
return (false);
}
LOG_DEBUG(gss_tsig_logger, DBGLVL_TRACE_BASIC, TKEY_EXCHANGE_VERIFIED);
return (true);
}
void
TKeyExchangeImpl::createTKeyRequest(const GssApiBufferPtr& outtoken) {
// Create a TKEY request.
msg_.reset(new Message(Message::RENDER));
// QID is added by IOFetch using the cryptolink::generateQid.
msg_->setOpcode(Opcode::QUERY());
msg_->setRcode(Rcode::NOERROR());
msg_->setHeaderFlag(Message::HEADERFLAG_QR, false);
msg_->setHeaderFlag(Message::HEADERFLAG_RD, false);
msg_->addQuestion(Question(key_->getKeyName(), RRClass::ANY(),
RRType::TKEY()));
// Create the TKEY Resource Record.
RRsetPtr tkey_rrset(new RRset(key_->getKeyName(), RRClass::ANY(),
RRType::TKEY(), RRTTL(0)));
Name algorithm("gss-tsig.");
uint32_t inception = key_->getInception32();
uint32_t expire = key_->getExpire32();
uint16_t mode = TKEY::GSS_API_MODE;
uint16_t error = Rcode::NOERROR().getCode();
size_t key_length = outtoken->getLength();
if (key_length > std::numeric_limits<uint16_t>::max()) {
isc_throw(BadValue, "TKEY value too long: " << key_length);
}
uint16_t key_len = static_cast<uint16_t>(key_length);
ConstRdataPtr tkey_rdata(new TKEY(algorithm, inception, expire, mode, error,
key_len, outtoken->getValue(), 0, 0));
tkey_rrset->addRdata(tkey_rdata);
msg_->addRRset(Message::SECTION_ADDITIONAL, tkey_rrset);
// Encode the TKEY request.
MessageRenderer renderer;
in_buf_.reset(new OutputBuffer(DEFAULT_BUFFER_SIZE));
renderer.setBuffer(in_buf_.get());
renderer.setLengthLimit(DEFAULT_BUFFER_SIZE);
msg_->toWire(renderer);
}
void
TKeyExchangeImpl::callCallback(TKeyExchange::Status status) {
// Once we are done with internal business, let's call a callback supplied
// by a caller.
if (callback_) {
(*callback_)(status);
}
if (status == TKeyExchange::SUCCESS) {
state_ = SUCCESS;
} else {
state_ = FAILURE;
}
}
void
TKeyExchangeImpl::doExchangeInternal(GssApiBufferPtr intoken) {
GssApiName named_gname(server_->getServerPrincipal());
GssApiBufferPtr outtoken(new GssApiBuffer());
OM_uint32 lifetime(0);
bool ret;
try {
ret = key_->getSecCtx().init(cred_, named_gname, flags_, *intoken,
*outtoken, lifetime);
} catch (const isc::Exception& ex) {
LOG_ERROR(gss_tsig_logger, TKEY_EXCHANGE_FAIL_TO_INIT)
.arg(ex.what());
incrStats("tkey-error");
callCallback(TKeyExchange::OTHER);
return;
}
if (ret) {
// Established.
if (!outtoken->empty()) {
// The RFC is not consistent about this case because it specifies it
// and at the same time requires the server to sign the last response
// so the first security context to be established is the server one
// and any further exchange does not make sense...
LOG_DEBUG(gss_tsig_logger, DBGLVL_TRACE_BASIC, TKEY_EXCHANGE_OUT_TOKEN_NOT_EMPTY);
}
lifetime = key_->getSecCtx().getLifetime();
if (lifetime < server_->getKeyLifetime()) {
ostringstream msg;
msg << "too short credential lifetime: " << lifetime
<< " < " << server_->getKeyLifetime();
LOG_ERROR(gss_tsig_logger, BAD_CLIENT_CREDENTIALS)
.arg(msg.str());
incrStats("tkey-error");
callCallback(TKeyExchange::BAD_CREDENTIALS);
return;
}
LOG_DEBUG(gss_tsig_logger, DBGLVL_TRACE_BASIC, TKEY_EXCHANGE_VALID)
.arg(key_->getSecCtx().getLifetime());
if (verifyTKey()) {
incrStats("tkey-success");
callCallback(TKeyExchange::SUCCESS);
} else {
incrStats("tkey-error");
callCallback(TKeyExchange::UNSIGNED_RESPONSE);
}
return;
}
if (outtoken->empty()) {
LOG_ERROR(gss_tsig_logger, TKEY_EXCHANGE_FAIL_EMPTY_OUT_TOKEN);
incrStats("tkey-error");
callCallback(TKeyExchange::INVALID_RESPONSE);
return;
}
createTKeyRequest(outtoken);
LOG_DEBUG(gss_tsig_logger, DBGLVL_TRACE_BASIC, TKEY_EXCHANGE_SEND_MESSAGE)
.arg(in_buf_->getLength());
incrStats("tkey-sent");
// Send the TKEY request.
IOAddress io_addr = server_->getIpAddress();
out_buf_.reset(new OutputBuffer(DEFAULT_BUFFER_SIZE));
// Do not use msg because IOFetch code will mess it!
io_fetch_.reset(new IOFetch(server_->getKeyProto(), io_service_, in_buf_,
io_addr, server_->getPort(), out_buf_,
this, static_cast<int>(timeout_)));
io_service_->post(*io_fetch_);
}
void
TKeyExchangeImpl::doExchange() {
if (state_ != NONE) {
isc_throw(InvalidOperation, "initiating exchange from invalid state");
}
state_ = STARTED;
// start acquire credentials
try {
acquireCredentials();
} catch (const isc::Exception& ex) {
LOG_ERROR(gss_tsig_logger, BAD_CLIENT_CREDENTIALS)
.arg(ex.what());
incrStats("tkey-error");
callCallback(TKeyExchange::BAD_CREDENTIALS);
return;
}
GssApiBufferPtr intoken(new GssApiBuffer());
doExchangeInternal(intoken);
}
void
TKeyExchangeImpl::cancel() {
if (io_fetch_) {
io_fetch_->stop();
}
state_ = STOPPED;
}
GssApiBufferPtr
TKeyExchangeImpl::readTKey(const OutputBufferPtr& outbuf) {
// Decode the TKEY response.
size_t msg_len = outbuf->getLength();
if (msg_len == 0) {
LOG_ERROR(gss_tsig_logger, TKEY_EXCHANGE_FAIL_EMPTY_RESPONSE);
return (GssApiBufferPtr());
}
const void* msg_buf = outbuf->getData();
if (!msg_buf) {
LOG_ERROR(gss_tsig_logger, TKEY_EXCHANGE_FAIL_NULL_RESPONSE);
return (GssApiBufferPtr());
}
LOG_DEBUG(gss_tsig_logger, DBGLVL_TRACE_BASIC, TKEY_EXCHANGE_RECEIVE_MESSAGE)
.arg(msg_len);
msg_.reset(new Message(Message::PARSE));
InputBuffer recv_buf(msg_buf, msg_len);
msg_->fromWire(recv_buf);
// Validate the TKEY response.
if (!msg_->getHeaderFlag(Message::HEADERFLAG_QR)) {
LOG_DEBUG(gss_tsig_logger, DBGLVL_TRACE_BASIC, TKEY_EXCHANGE_NOT_A_RESPONSE);
}
if (msg_->getRcode() != Rcode::NOERROR()) {
LOG_ERROR(gss_tsig_logger, TKEY_EXCHANGE_FAIL_RESPONSE_ERROR)
.arg(msg_->getRcode().toText());
return (GssApiBufferPtr());
}
if (msg_->getOpcode() != Opcode::QUERY()) {
LOG_ERROR(gss_tsig_logger, TKEY_EXCHANGE_FAIL_WRONG_RESPONSE_OPCODE)
.arg(msg_->getOpcode());
return (GssApiBufferPtr());
}
// According to RFC3645, the TKEY is found in the ANSWER section.
if (msg_->getRRCount(Message::SECTION_ANSWER) != 1) {
LOG_ERROR(gss_tsig_logger, TKEY_EXCHANGE_FAIL_WRONG_RESPONSE_ANSWER_COUNT)
.arg(msg_->getRRCount(Message::SECTION_ANSWER));
return (GssApiBufferPtr());
}
RRsetPtr rrset = *msg_->beginSection(Message::SECTION_ANSWER);
if (!rrset) {
LOG_ERROR(gss_tsig_logger, TKEY_EXCHANGE_FAIL_NO_RESPONSE_ANSWER);
return (GssApiBufferPtr());
}
if (rrset->getClass() != RRClass::ANY()) {
LOG_DEBUG(gss_tsig_logger, DBGLVL_TRACE_BASIC, TKEY_EXCHANGE_ANSWER_CLASS)
.arg(rrset->getClass().toText());
}
if (rrset->getType() != RRType::TKEY()) {
LOG_ERROR(gss_tsig_logger, TKEY_EXCHANGE_FAIL_WRONG_RESPONSE_ANSWER_TYPE)
.arg(rrset->getType().toText());
return (GssApiBufferPtr());
}
if (rrset->getTTL() != RRTTL(0)) {
LOG_DEBUG(gss_tsig_logger, DBGLVL_TRACE_BASIC, TKEY_EXCHANGE_RESPONSE_TTL)
.arg(rrset->getTTL().toText());
}
if (rrset->getRdataCount() != 1) {
LOG_DEBUG(gss_tsig_logger, DBGLVL_TRACE_BASIC, TKEY_EXCHANGE_RDATA_COUNT)
.arg(rrset->getRdataCount());
if (rrset->getRdataCount() == 0) {
LOG_ERROR(gss_tsig_logger, TKEY_EXCHANGE_FAIL_NO_RDATA);
return (GssApiBufferPtr());
}
}
auto rdata_it = rrset->getRdataIterator();
const TKEY& tkey = dynamic_cast<const TKEY&>(rdata_it->getCurrent());
if (tkey.getError() != Rcode::NOERROR_CODE) {
LOG_ERROR(gss_tsig_logger, TKEY_EXCHANGE_FAIL_TKEY_ERROR)
.arg(tkey.getError());
return (GssApiBufferPtr());
}
return (GssApiBufferPtr(new GssApiBuffer(tkey.getKeyLen(), tkey.getKey())));
}
void
TKeyExchangeImpl::incrStats(const string& stat) {
StatsMgr& mgr = StatsMgr::instance();
mgr.addValue(stat, static_cast<int64_t>(1));
if (server_) {
mgr.addValue(StatsMgr::generateName("server", server_->getID(), stat),
static_cast<int64_t>(1));
}
}
const OM_uint32 TKeyExchange::TKEY_EXCHANGE_FLAGS = (GSS_C_REPLAY_FLAG | GSS_C_MUTUAL_FLAG | GSS_C_INTEG_FLAG);
const uint32_t TKeyExchange::TKEY_EXCHANGE_IO_TIMEOUT = 3000;
TKeyExchange::TKeyExchange(const IOServicePtr& io_service,
const DnsServerPtr& server, const GssTsigKeyPtr& key,
Callback* callback, uint32_t timeout, OM_uint32 flags)
: impl_(new TKeyExchangeImpl(io_service, server, key, callback, timeout, flags)) {
}
TKeyExchange::~TKeyExchange() {
cancel();
}
void
TKeyExchange::doExchange() {
impl_->doExchange();
}
void
TKeyExchange::cancel() {
impl_->cancel();
}
IOServicePtr
TKeyExchange::getIOService() {
return (impl_->getIOService());
}
void
TKeyExchange::setIOService(const IOServicePtr io_service) {
impl_->setIOService(io_service);
}
} // namespace gss_tsig
} // namespace isc

View File

@ -0,0 +1,131 @@
// Copyright (C) 2021-2022 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 TKEY_EXCHANGE_H
#define TKEY_EXCHANGE_H
#include <asiodns/io_fetch.h>
#include <asiolink/io_service.h>
#include <gss_tsig_cfg.h>
#include <gss_tsig_key.h>
#include <util/buffer.h>
#include <memory>
namespace isc {
namespace gss_tsig {
class TKeyExchange;
typedef boost::shared_ptr<TKeyExchange> TKeyExchangePtr;
/// GssTKeyExchange class implementation.
class TKeyExchangeImpl;
/// @brief The @c TKeyExchange class handles communication with the DNS server.
///
/// Communication with the DNS server is asynchronous. Caller must provide a
/// callback, which will be invoked when the response from the DNS server is
/// received, a timeout has occurred or IO service has been stopped for any
/// reason. The caller-supplied callback is called by the internal callback
/// operator implemented by @c TKeyExchange. This callback is responsible for
/// initializing the @c GssTsigContext instance which encapsulates the response
/// from the DNS. This initialization does not take place if the response from
/// DNS is not received.
class TKeyExchange : boost::noncopyable {
public:
/// @brief A status code of the TKeyExchange.
enum Status {
SUCCESS, ///< Response received and is ok.
TIMEOUT, ///< No response, timeout.
IO_STOPPED, ///< IO was stopped.
INVALID_RESPONSE, ///< Response received but invalid.
UNSIGNED_RESPONSE, ///< Response received but not signed.
BAD_CREDENTIALS, ///< Bad client credentials.
OTHER ///< Other, unclassified error.
};
/// @brief Convert a status to its textual form.
static std::string statusToText(Status status);
/// @brief Callback for the @c TKeyExchange class.
///
/// This is an abstract class which represents the external callback for the
/// @c TKeyExchange. Caller must implement this class and supply its instance
/// in the @c TKeyExchange constructor to get callbacks when the TKEY
/// exchange is complete (@see @c TKeyExchange).
class Callback {
public:
/// @brief Virtual destructor.
virtual ~Callback() { }
/// @brief Function operator implementing a callback.
///
/// @param status a @c TKeyExchange::Status enum representing status code
/// of TKeyExchange operation.
virtual void operator()(TKeyExchange::Status status) = 0;
};
/// @brief Constructor.
///
/// @param io_service The IOService which handles IO operations.
/// @param server The server for which the TKEY exchange is performed.
/// @param key The TKEY being updated.
/// @param callback Pointer to an object implementing
/// @c TKeyExchange::Callback class. This object will be called when DNS
/// message exchange completes or if an error occurs. NULL value disables
/// callback invocation.
/// @param timeout The timeout for the IO operations.
/// @param flags The flags used for the TKEY exchange.
/// @throw BadValue if io_service is null.
/// @throw BadValue if key's security context has already been used.
TKeyExchange(const isc::asiolink::IOServicePtr& io_service,
const DnsServerPtr& server, const GssTsigKeyPtr& key,
Callback* callback, uint32_t timeout = TKEY_EXCHANGE_IO_TIMEOUT,
OM_uint32 flags = TKEY_EXCHANGE_FLAGS);
/// @brief Virtual destructor, does nothing.
virtual ~TKeyExchange();
/// @brief This function handles the repeated communication with the DNS
/// server trying to complete the TKEY exchange.
void doExchange();
/// @brief This function cancels the in-flight TKEY exchange.
void cancel();
/// @brief Gets IO service.
///
/// @return IOService object, used for all ASIO operations.
isc::asiolink::IOServicePtr getIOService();
/// @brief Sets IO service.
///
/// @param io_service IOService object, used for all ASIO operations.
void setIOService(const isc::asiolink::IOServicePtr io_service);
/// @brief The default TKEY exchange flags.
///
/// The default flags consist in:
/// - GSS_C_MUTUAL_FLAG: A flag that requires both of initiator and acceptor
/// to be authenticated.
/// - GSS_C_REPLAY_FLAG: A flag that detects repeated messages.
/// - GSS_C_INTEG_FLAG: A flag that makes integrity services (that is,
/// cryptographic signatures) available for transferred messages.
/// Note the GSS_C_SEQUENCE_FLAG is not set by default.
static const OM_uint32 TKEY_EXCHANGE_FLAGS;
/// @brief The default IO timeout used for IO operations (in milliseconds)
/// set to 3000 (3 seconds).
static const uint32_t TKEY_EXCHANGE_IO_TIMEOUT;
private:
/// @brief Smart pointer to TKeyExchange implementation.
std::unique_ptr<TKeyExchangeImpl> impl_;
};
} // namespace isc
} // namespace gss_tsig
#endif // TKEY_EXCHANGE_H

View File

@ -0,0 +1,17 @@
// Copyright (C) 2021-2022 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);
}
}

1
src/hooks/d2/meson.build Normal file
View File

@ -0,0 +1 @@
subdir('gss_tsig')

View File

@ -1,4 +1,4 @@
SUBDIRS = bootp flex_option high_availability lease_cmds perfmon
SUBDIRS = bootp class_cmds ddns_tuning flex_id flex_option high_availability host_cache host_cmds lease_cmds limits perfmon ping_check
if HAVE_MYSQL
SUBDIRS += mysql
@ -8,4 +8,4 @@ if HAVE_PGSQL
SUBDIRS += pgsql
endif
SUBDIRS += run_script stat_cmds user_chk
SUBDIRS += forensic_log lease_query radius run_script stat_cmds subnet_cmds user_chk

View File

@ -1,4 +1,8 @@
# Copyright (C) 2019-2024 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/.
% BOOTP_BOOTP_QUERY recognized a BOOTP query: %1
Logged at debug log level 40.

View File

@ -0,0 +1,2 @@
/class_cmds_messages.cc -diff merge=ours
/class_cmds_messages.h -diff merge=ours

1
src/hooks/dhcp/class_cmds/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/html

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
# This is a doxygen configuration for generating XML output as well as HTML.
#
# Inherit everything from our default Doxyfile except GENERATE_XML, which
# will be reset to YES
@INCLUDE = Doxyfile
GENERATE_XML = YES

View File

@ -0,0 +1,91 @@
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 is included in the distribution
EXTRA_DIST = class_cmds_messages.mes
CLEANFILES = *.gcno *.gcda
# convenience archive
noinst_LTLIBRARIES = libclass_cmds.la
libclass_cmds_la_SOURCES = class_cmds.cc class_cmds.h
libclass_cmds_la_SOURCES += class_cmds_callouts.cc
libclass_cmds_la_SOURCES += class_cmds_log.cc class_cmds_log.h
libclass_cmds_la_SOURCES += class_cmds_messages.cc class_cmds_messages.h
libclass_cmds_la_SOURCES += version.cc
libclass_cmds_la_CXXFLAGS = $(AM_CXXFLAGS)
libclass_cmds_la_CPPFLAGS = $(AM_CPPFLAGS)
# install the shared object into $(libdir)/kea/hooks
lib_hooksdir = $(libdir)/kea/hooks
lib_hooks_LTLIBRARIES = libdhcp_class_cmds.la
libdhcp_class_cmds_la_SOURCES =
libdhcp_class_cmds_la_LDFLAGS = $(AM_LDFLAGS)
libdhcp_class_cmds_la_LDFLAGS += -avoid-version -export-dynamic -module
libdhcp_class_cmds_la_LIBADD = libclass_cmds.la
libdhcp_class_cmds_la_LIBADD += $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la
libdhcp_class_cmds_la_LIBADD += $(top_builddir)/src/lib/process/libkea-process.la
libdhcp_class_cmds_la_LIBADD += $(top_builddir)/src/lib/eval/libkea-eval.la
libdhcp_class_cmds_la_LIBADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la
libdhcp_class_cmds_la_LIBADD += $(top_builddir)/src/lib/stats/libkea-stats.la
libdhcp_class_cmds_la_LIBADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la
libdhcp_class_cmds_la_LIBADD += $(top_builddir)/src/lib/http/libkea-http.la
libdhcp_class_cmds_la_LIBADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la
libdhcp_class_cmds_la_LIBADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la
libdhcp_class_cmds_la_LIBADD += $(top_builddir)/src/lib/cc/libkea-cc.la
libdhcp_class_cmds_la_LIBADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
libdhcp_class_cmds_la_LIBADD += $(top_builddir)/src/lib/dns/libkea-dns++.la
libdhcp_class_cmds_la_LIBADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la
libdhcp_class_cmds_la_LIBADD += $(top_builddir)/src/lib/log/libkea-log.la
libdhcp_class_cmds_la_LIBADD += $(top_builddir)/src/lib/util/libkea-util.la
libdhcp_class_cmds_la_LIBADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
EXTRA_DIST += class_cmds.dox Doxyfile Doxyfile-xml
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 class_cmds_messages.h class_cmds_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: class_cmds_messages.h class_cmds_messages.cc
@echo Message files regenerated
class_cmds_messages.h class_cmds_messages.cc: class_cmds_messages.mes
$(top_builddir)/src/lib/log/compiler/kea-msg-compiler $(top_srcdir)/src/hooks/dhcp/class_cmds/class_cmds_messages.mes
else
messages class_cmds_messages.h class_cmds_messages.cc:
@echo Messages generation disabled. Configure with --enable-generate-messages to enable it.
endif

View File

@ -0,0 +1,531 @@
// Copyright (C) 2020-2023 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 <class_cmds.h>
#include <class_cmds_log.h>
#include <cc/command_interpreter.h>
#include <dhcpsrv/cfgmgr.h>
#include <dhcpsrv/client_class_def.h>
#include <dhcpsrv/parsers/client_class_def_parser.h>
#include <util/multi_threading_mgr.h>
#include <string>
#include <sstream>
using namespace isc::config;
using namespace isc::dhcp;
using namespace isc::data;
using namespace isc::hooks;
using namespace isc::util;
using namespace std;
namespace isc {
namespace class_cmds {
/// @brief Implementation of the @c ClassCmds class.
///
/// It provides functions for class manipulations.
class ClassCmdsImpl {
public:
/// @brief Constructor.
ClassCmdsImpl() {
family_ = CfgMgr::instance().getFamily();
}
/// @brief Destructor.
~ClassCmdsImpl() { }
private:
/// @brief Retrieves mandatory arguments from the command encapsulated
/// within the callout handle.
///
/// @param callout_handle Callout handle encapsulating the command.
///
/// @return Pointer to the data structure encapsulating arguments being
/// a map.
/// @throw CtrlChannelError if the arguments are not present or aren't
/// a map.
ConstElementPtr getMandatoryArguments(CalloutHandle& callout_handle) const {
ConstElementPtr command;
callout_handle.getArgument("command", command);
ConstElementPtr arguments;
static_cast<void>(parseCommandWithArgs(arguments, command));
return (arguments);
}
/// @brief Parses and validates arguments of the class-add and class-update
/// commands.
///
/// @param command_name Command name, used for error reporting.
/// @param arguments Command arguments to be parsed and validated.
///
/// @return JSON list of classes. Currently only one class is allowed
/// on the list.
ConstElementPtr parseReceivedClass(const std::string& command_name,
const ConstElementPtr& arguments) {
// Arguments must have one entry.
if (arguments->size() != 1) {
isc_throw(BadValue,
"invalid number of arguments " << arguments->size()
<< " for the '" << command_name << "' command. "
<< "Expecting 'client-classes' list");
}
// The map must contain a 'client-classes' list.
ConstElementPtr list = arguments->get("client-classes");
if (!list) {
isc_throw(BadValue,
"missing 'client-classes' argument for the '"
<< command_name << "' command");
}
// Make sure it is a list.
if (list->getType() != Element::list) {
isc_throw(BadValue,
"'client-classes' argument specified for the '"
<< command_name << "' command is not a list");
}
// Currently we allow only one class in the list.
if (list->size() != 1) {
isc_throw(BadValue,
"invalid number of classes specified for the '"
<< command_name << "' command. Expected one class");
}
return (list);
}
public:
/// @brief Returns a response to a 'class-add' command.
///
/// @param callout_handle Reference to the callout handle holding command
/// to be processed and where result should be stored.
void addClass(CalloutHandle& callout_handle) {
ConstElementPtr response;
try {
// Fetch command arguments.
ConstElementPtr arguments = getMandatoryArguments(callout_handle);
// Parse arguments and extract the class list. This list
// is expected to contain exactly one class.
ConstElementPtr class_list = parseReceivedClass("class-add", arguments);
// Make sure that the class definition is a map.
ConstElementPtr class_def = class_list->get(0);
if (class_def->getType() != Element::map) {
isc_throw(BadValue,
"invalid class definition specified for the "
"'class-add' command. Expected a map");
}
// Make sure that there are no unsupported parameters in the client
// class definition.
ClientClassDefParser parser;
parser.checkParametersSupported(class_def, family_);
// Finally, let's parse the class definition extended with the
// default values.
ClientClassDictionaryPtr dictionary =
CfgMgr::instance().getCurrentCfg()->getClientClassDictionary();
parser.parse(dictionary, class_def, family_, false);
// Add text stating that client class has been added.
// This is an additional sanity check. This should be caught by
// the ClientClassDefParser earlier (and throw if name is missing)
if (!class_def->contains("name")) {
isc_throw(BadValue, "missing 'name' argument for the 'class-get' command");
}
ostringstream text;
string name = class_def->get("name")->stringValue();
text << "Class '" << name << "' added";
// Create the response.
response = createAnswer(CONTROL_RESULT_SUCCESS, text.str());
LOG_INFO(class_cmds_logger, CLASS_CMDS_CLASS_ADD).arg(class_def->str());
} catch (const std::exception& ex) {
LOG_INFO(class_cmds_logger, CLASS_CMDS_CLASS_ADD_FAILED)
.arg(ex.what());
response = createAnswer(CONTROL_RESULT_ERROR, ex.what());
}
callout_handle.setArgument("response", response);
}
/// @brief Returns a response to a 'class-get' command
///
/// @param callout_handle Reference to the callout handle holding command
/// to be processed and where result should be stored.
void getClass(CalloutHandle& callout_handle) const {
ConstElementPtr response;
try {
// Fetch command arguments.
ConstElementPtr arguments = getMandatoryArguments(callout_handle);
// Arguments must have one entry.
if (arguments->size() != 1) {
isc_throw(BadValue,
"invalid number of arguments " << arguments->size()
<< " for the 'class-get' command. "
<< "Expecting 'name' string");
}
// The map must contain a 'name' string.
ConstElementPtr name = arguments->get("name");
if (!name) {
isc_throw(BadValue,
"missing 'name' argument for the 'class-get' command");
}
// Make sure it is a string.
if (name->getType() != Element::string) {
isc_throw(BadValue,
"'name' argument specified for the 'class-get' "
"command is not a string");
}
string name_str = name->stringValue();
ClientClassDictionaryPtr dictionary =
CfgMgr::instance().getCurrentCfg()->getClientClassDictionary();
ClientClassDefPtr def = dictionary->findClass(name_str);
// If class found, wrap its definition in the successful response.
if (def) {
ElementPtr list = Element::createList();
list->add(def->toElement());
ElementPtr map = Element::createMap();
map->set("client-classes", list);
ostringstream text;
text << "Class '" << name_str << "' definition returned";
response = createAnswer(CONTROL_RESULT_SUCCESS, text.str(), map);
LOG_INFO(class_cmds_logger, CLASS_CMDS_CLASS_GET).arg(name_str);
} else {
ostringstream text;
text << "Class '" << name_str << "' not found";
response = createAnswer(CONTROL_RESULT_EMPTY, text.str());
LOG_INFO(class_cmds_logger, CLASS_CMDS_CLASS_GET_EMPTY).arg(name_str);
}
} catch (const std::exception& ex) {
LOG_INFO(class_cmds_logger, CLASS_CMDS_CLASS_GET_FAILED)
.arg(ex.what());
response = createAnswer(CONTROL_RESULT_ERROR, ex.what());
}
callout_handle.setArgument("response", response);
}
/// @brief Returns a response to a 'class-list' command.
///
/// @param callout_handle Reference to the callout handle holding command
/// to be processed and where result should be stored.
void getClassList(CalloutHandle& callout_handle) const {
ConstElementPtr response;
try {
// Create a list where we're going to store classes' names.
ElementPtr list = Element::createList();
// Create arguments map and add client classes map.
ElementPtr args = Element::createMap();
args->set("client-classes", list);
// Retrieve all classes from the configuration structure.
auto const& classes = CfgMgr::instance().getCurrentCfg()->
getClientClassDictionary()->getClasses();
// Iterate over all classes and retrieve names.
for (auto const& c : *classes) {
ElementPtr entry = Element::createMap();
entry->set("name", Element::create(c->getName()));
list->add(entry);
}
// Generate the status message including the number of classes found.
ostringstream text;
text << classes->size() << " class";
// For 0 classes or more than 1 classes returned, we use plural form 'classes'.
if (classes->size() != 1) {
text << "es";
}
text << " found";
if (classes->size() > 0) {
response = createAnswer(CONTROL_RESULT_SUCCESS, text.str(), args);
LOG_INFO(class_cmds_logger, CLASS_CMDS_CLASS_LIST);
} else {
response = createAnswer(CONTROL_RESULT_EMPTY, text.str(),args);
LOG_INFO(class_cmds_logger, CLASS_CMDS_CLASS_LIST_EMPTY);
}
} catch (const std::exception& ex) {
LOG_INFO(class_cmds_logger, CLASS_CMDS_CLASS_LIST_FAILED)
.arg(ex.what());
response = createAnswer(CONTROL_RESULT_ERROR, ex.what());
}
callout_handle.setArgument("response", response);
}
/// @brief Returns a response to a 'class-update' command
///
/// @param callout_handle Reference to the callout handle holding command
/// to be processed and where result should be stored.
void updateClass(CalloutHandle& callout_handle) {
ConstElementPtr response;
try {
// Fetch command arguments.
ConstElementPtr arguments = getMandatoryArguments(callout_handle);
// Parse arguments and extract the class list. This list
// is expected to contain exactly one class.
ConstElementPtr class_list = parseReceivedClass("class-update", arguments);
// Make sure that the class definition is a map.
ConstElementPtr class_def = class_list->get(0);
if (class_def->getType() != Element::map) {
isc_throw(BadValue,
"invalid class definition specified for the "
"'class-update' command. Expected a map");
}
// Check the class is configured. But before we get to that, need to
// do some basic sanity check first.
if (!class_def->contains("name")) {
isc_throw(BadValue, "the first class definition is missing a "
"mandatory 'name' parameter");
}
// Make sure that there are no unsupported parameters in the client
// class definition.
ClientClassDefParser parser;
parser.checkParametersSupported(class_def, family_);
string name = class_def->get("name")->stringValue();
ClientClassDictionaryPtr dictionary =
CfgMgr::instance().getCurrentCfg()->getClientClassDictionary();
ClientClassDefPtr previous = dictionary->findClass(name);
if (previous) {
auto const& template_cfg = class_def->get("template-test");
if (!template_cfg && dynamic_cast<TemplateClientClassDef*>(previous.get())) {
ElementPtr mutable_class_def = boost::const_pointer_cast<Element>(class_def);
mutable_class_def->set("template-test", Element::create(previous->getTest()));
}
} else {
ostringstream text;
text << "Class '" << name << "' is not found";
response = createAnswer(CONTROL_RESULT_EMPTY, text.str());
callout_handle.setArgument("response", response);
LOG_INFO(class_cmds_logger, CLASS_CMDS_CLASS_UPDATE_EMPTY).arg(name);
return;
}
// Build the before and after part of the dictionary.
ClientClassDictionaryPtr before(new ClientClassDictionary());
ClientClassDictionaryPtr after(new ClientClassDictionary());
ClientClassDefListPtr classes = dictionary->getClasses();
bool found = false;
for (auto const& it : *classes) {
if (it->getName() == name) {
found = true;
} else if (found) {
after->addClass(it);
} else {
before->addClass(it);
}
}
// Finally, let's parse the class definition extended with the
// default values.
parser.parse(before, class_def, family_, false);
ClientClassDefPtr next = before->findClass(name);
if (!next) {
isc_throw(Unexpected,
"Class '" << name << "' updated but does not show");
}
// The dependency on the KNOWN or UNKNOWN built-in class must not
// change during the class update. The reason is that such dependency
// may be indirect, i.e. some other class may depend on those built-in
// classes via this class.
if (previous->getDependOnKnown() != next->getDependOnKnown()) {
isc_throw(DependOnKnownError,
"modification of the class '" << name << "' would "
"affect its dependency on the KNOWN and/or UNKNOWN built-in "
"classes. Such modification is not allowed because "
"there may be other classes depending on those built-ins "
"via the updated class");
}
// Merge after part.
classes = after->getClasses();
for (auto const& it : *classes) {
before->addClass(it);
}
// Set the dictionary.
CfgMgr::instance().getCurrentCfg()->setClientClassDictionary(before);
// Add text stating that client class has been updated.
ostringstream text;
text << "Class '" << name << "' updated";
// Create the response.
response = createAnswer(CONTROL_RESULT_SUCCESS, text.str());
LOG_INFO(class_cmds_logger, CLASS_CMDS_CLASS_UPDATE).arg(class_def->str());
} catch (const std::exception& ex) {
LOG_INFO(class_cmds_logger, CLASS_CMDS_CLASS_UPDATE_FAILED)
.arg(ex.what());
response = createAnswer(CONTROL_RESULT_ERROR, ex.what());
}
callout_handle.setArgument("response", response);
}
/// @brief Processes and returns a response to 'class-del' command.
///
/// @param callout_handle Reference to the callout handle holding command
/// to be processed and where result should be stored.
void delClass(CalloutHandle& callout_handle) {
ConstElementPtr response;
try {
// Fetch command arguments.
ConstElementPtr arguments = getMandatoryArguments(callout_handle);
// Arguments must have one entry.
if (arguments->size() != 1) {
isc_throw(BadValue,
"invalid number of arguments " << arguments->size()
<< " for the 'class-del' command. "
<< "Expecting 'name' string");
}
// The map must contain a 'name' string.
ConstElementPtr name = arguments->get("name");
if (!name) {
isc_throw(BadValue,
"missing 'name' argument for the 'class-del' command");
}
// Make sure it is a string.
if (name->getType() != Element::string) {
isc_throw(BadValue,
"'name' argument specified for the 'class-del' "
"command is not a string");
}
string name_str = name->stringValue();
ClientClassDictionaryPtr dictionary =
CfgMgr::instance().getCurrentCfg()->getClientClassDictionary();
ClientClassDefPtr def = dictionary->findClass(name_str);
ostringstream text;
// Check if the class was found.
if (!def) {
text << "Class '" << name_str << "' not found";
response = createAnswer(CONTROL_RESULT_EMPTY, text.str());
callout_handle.setArgument("response", response);
LOG_INFO(class_cmds_logger, CLASS_CMDS_CLASS_DEL_EMPTY).arg(name_str);
return;
}
// Check against dangling dependencies.
string depend;
if (dictionary->dependOnClass(name_str, depend)) {
isc_throw(InUseError,
"Class '" << name_str << "' is used by class '"
<< depend << "'");
}
// Remove the class from the configuration.
dictionary->removeClass(name_str);
text << "Class '" << name_str << "' deleted";
response = createAnswer(CONTROL_RESULT_SUCCESS, text.str());
LOG_INFO(class_cmds_logger, CLASS_CMDS_CLASS_DEL).arg(name_str);
} catch (const std::exception& ex) {
LOG_INFO(class_cmds_logger, CLASS_CMDS_CLASS_DEL_FAILED)
.arg(ex.what());
response = createAnswer(CONTROL_RESULT_ERROR, ex.what());
}
callout_handle.setArgument("response", response);
}
private:
/// @brief Protocol family (IPv4 or IPv6)
uint16_t family_;
};
ClassCmds::ClassCmds()
:impl_(new ClassCmdsImpl()) {
}
void
ClassCmds::getClass(CalloutHandle& callout_handle) const {
impl_->getClass(callout_handle);
}
void
ClassCmds::getClassList(CalloutHandle& callout_handle) const {
impl_->getClassList(callout_handle);
}
// Modifying the class configuration requires a critical section.
void
ClassCmds::addClass(CalloutHandle& callout_handle) {
MultiThreadingCriticalSection sc;
impl_->addClass(callout_handle);
}
void
ClassCmds::updateClass(CalloutHandle& callout_handle) {
MultiThreadingCriticalSection sc;
impl_->updateClass(callout_handle);
}
void
ClassCmds::delClass(CalloutHandle& callout_handle) {
MultiThreadingCriticalSection sc;
impl_->delClass(callout_handle);
}
}
}

View File

@ -0,0 +1,120 @@
// Copyright (C) 2018-2022 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 Class Commands Hooks Library
Welcome to Kea Class Commands Hooks Library. This documentation is
addressed at developers who are interested in internal operation of the
Class Commands 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 class_cmds Overview
## Introduction
libdhcp_class_cmds is a hooks library which provides additional commands
for manipulating client classes. The currently supported commands are:
- class-get - which attempts to retrieve client class with specified name.
Details of the client class, if present, will be returned.
- class-list - which attempts to retrieve the list of all client class names.
- class-add - inserts a new client class. This allows you to
dynamically (while the server is running) insert a new client class
with test expressions, options, several DHCPv4 message fields, ...
- class-update - updates a configured client class. This allows you to
dynamically (while the server is running) update existing client class
with new test expression, options, several DHCPv4 message fields, ...
- class-del - deletes existing client class. This allows you to remove
an existing class while the server is running.
## Internal structure
Almost all the code is enclosed within isc::class_cmds namespace.
The core library consists of several files:
- class_cmds.h - This file contains definition of a @c isc::class_cmds::ClassCmds
class that contains handlers for specific operations: @c addClass for
adding new classes, @c getClass for retrieving existing class,
@c getClassList for retrieving names of existing classes, @c updateClass
for updating existing class and @c delClass for deleting a client class.
This class uses pimpl design pattern, which means that the actual
implementation is hidden in an internal object. The advantage of
this approach is mostly hermetization, i.e. the internals are not
exposed and the whole library provides small, clean interface. There
is a pointer to internal class called @c ClassCmdsImpl. That class is
defined in class_cmds.cc.
- class_cmds.cc - This file contains the most important piece: code that
performs the operations on classes.
- class_cmds_callouts.cc - This is a very simple file that provides wrappers for
hook points. Due to the way how hooks interface is defined in Kea, these are
plain C-style functions (note extern "C" clause and lack of namespaces). This
file also contains @ref load() and @ref unload() functions. The load function
registers new commands (class-add, class-get, class-list, class-update
and class-del).
- version.cc - This file contains a single function that returns Kea hooks
version. This is part of the mandatory hooks library interface and is used
by Kea to check if the library is not compiled against too old or too new
version.
- class_cmds_messages.cc - This file is autogenerated and should never be
modified. If you want to introduce any changes, please modify
class_cmds_messages.mes instead.
- class_cmds_messages.h - This file is autogenerated and should never be
modified. If you want to introduce any changes, please modify
class_cmds_messages.mes instead.
- class_cmds_messages.mes - This file contains list of all message the library
could print, with a short description of what specific message means.
- class_cmds_log.h - This file contains declaration of a logger that is used
in class_cmds library.
- class_cmds_log.cc - This file contains definition of a logger that is used
in class_cmds library.
- class_cmds.dox - This doxygen documentation contains main part of the Developer's
Guide text.
## Regenerating this documentation
To regenerate this documentation, type: make devel in the main directory of the
class_cmds library. Make sure you have at least doxygen software installed, but it
is useful to also have graphviz. The generated documentation will be stored
in html/ directory.
## Testing
Similar to all other code in Kea, also this library comes with unit-tests that
use googletest framework. Those tests are stored in tests/ directory. To build
and run them, you need to pass --with-gtest or --with-gtest-source to the
configure script. Once the code builds, you can run tests with make check. This
command can be run in top-level Kea directory (all tests will be run) or in more
specific directory (only a subset of tests will be run). For example, make check
issued in src/hooks/dhcp/class_cmds will run only tests for class_cmds
library.
@section class_cmdsMTCompatibility Multi-Threading Compatibility
The libdhcp_class_cmds hooks library is compatible with multi-threading.
All commands are executed inside a critical section, i.e. with threads stopped.
*/

View File

@ -0,0 +1,228 @@
// Copyright (C) 2020-2023 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 CLASS_CMDS_H
#define CLASS_CMDS_H
#include <exceptions/exceptions.h>
#include <cc/data.h>
#include <hooks/hooks.h>
#include <boost/shared_ptr.hpp>
namespace isc {
namespace class_cmds {
/// @brief Thrown upon an attempt to update a class when dependency
/// on KNOWN or UNKNOWN built-in class is removed or added.
///
/// Such modification is not allowed because that may affect other
/// classes which depend on those built-in classes via the
/// modified class.
class DependOnKnownError : public isc::Exception {
public:
DependOnKnownError(const char* file, size_t line, const char* what) :
isc::Exception(file, line, what)
{}
};
/// @brief Thrown upon an attempt to delete a class which would
/// result in leaving dangling dependencies.
class InUseError : public isc::Exception {
public:
InUseError(const char* file, size_t line, const char* what) :
isc::Exception(file, line, what)
{}
};
/// @brief Forward declaration of implementation class.
class ClassCmdsImpl;
/// @brief Implements the logic for processing commands pertaining to
/// client classes manipulation.
///
/// This class is used by the callouts implementing command handlers for
/// client classes manipulations.
///
/// Commands which add and update client classes enclose the classes in
/// a JSON list, even though we currently allow only one class to be
/// added or updated per command. In the future we may allow more classes
/// to be added or updated with a single command, but this will
/// require implementation of some sort of transaction mechanism, so as
/// an error in processing one of the classes would cause the rollback
/// of other changes. Allowing partial update could cause inconsistency
/// in the server configuration.
///
/// Meanwhile, we allow only one class to be added or updated but the
/// command structure is already prepared for possible future extension.
class ClassCmds {
public:
/// @brief Constructor.
///
/// It creates an instance of the @c ClassCmdsImpl.
ClassCmds();
/// @brief Returns a response to a 'class-add' command
///
/// This function processes 'class-add' command by adding it to the
/// current server configuration and returns a response to the client.
///
/// The command has the following structure:
/// @code
/// {
/// "command": "class-add",
/// "arguments": {
/// "client-classes": [
/// {
/// "name": "my-class",
/// ...
/// }
/// ]
/// }
/// }
/// @endcode
///
/// Before adding the class to the current configuration this method
/// will check for duplicated class, i.e. having the same name, and
/// expressions referring to unspecified (i.e. not builtin nor configured)
/// classes.
///
/// The successful response has the following structure:
/// @code
/// {
/// "result": 0,
/// "text": "Class 'my-class' added."
/// }
/// @endcode
///
/// @param callout_handle Reference to the callout handle holding command
/// to be processed and where result should be stored.
void addClass(hooks::CalloutHandle& callout_handle);
/// @brief Returns a response to a 'class-get' command
///
/// This function returns a client class by a name.
///
/// The successful response has the following structure:
/// @code
/// {
/// "result": 0,
/// "text": "Class 'my-class' definition returned",
/// "arguments": {
/// "client-classes": [
/// {
/// "name": "my-class",
/// ...
/// }
/// ]
/// }
/// }
/// @endcode
///
/// @param callout_handle Reference to the callout handle holding command
/// to be processed and where result should be stored.
void getClass(hooks::CalloutHandle& callout_handle) const;
/// @brief Returns a response to a 'class-list' command.
///
/// This method retrieves configured client classes and returns the list
/// of their names.
///
/// The successful response has the following structure:
/// @code
/// {
/// "result": 0,
/// "text": "2 classes found",
/// "arguments": {
/// "client-classes": [
/// {
/// "name": "classA"
/// },
/// {
/// "name": "classB"
/// }
/// ]
/// }
/// }
/// @endcode
///
/// @param callout_handle Reference to the callout handle holding command
/// to be processed and where result should be stored.
void getClassList(hooks::CalloutHandle& callout_handle) const;
/// @brief Returns a response to a 'class-update' command
///
/// This function processes 'class-update' command by updating it to the
/// current server configuration and returns a response to the client.
///
/// The command has the following structure:
/// @code
/// {
/// "command": "class-update",
/// "arguments": {
/// "client-classes": [
/// {
/// "name": "my-class",
/// ...
/// }
/// ]
/// }
/// }
/// @endcode
///
/// Before updating the class to the current configuration this method
/// will check for duplicated class, i.e. having the same name, and
/// expressions referring to unspecified (i.e. not builtin nor configured)
/// and forward (i.e. configured after) classes. Dependency to KNOWN
/// and UNKNOWN builtin classes must not change.
///
/// The successful response has the following structure:
/// @code
/// {
/// "result": 0,
/// "text": "Class 'my-class' updated."
/// }
/// @endcode
///
/// @param callout_handle Reference to the callout handle holding command
/// to be processed and where result should be stored.
void updateClass(hooks::CalloutHandle& callout_handle);
/// @brief Processes and returns a response to 'class-del' command.
///
/// This function processes the 'class-del' command by searching for a
/// class by specified name and deleting this class from the server
/// configuration.
/// The command has the following structure:
/// @code
/// {
/// "command": "class-del",
/// "arguments": {
/// {
/// "name": "my-class"
/// }
/// }
/// }
/// @endcode
///
/// If the class exists and is referred by another class an error is
/// returned with the name of this class.
///
/// @param callout_handle Reference to the callout handle holding command
/// to be processed and where result should be stored.
void delClass(hooks::CalloutHandle& callout_handle);
private:
/// Pointer to the actual implementation
boost::shared_ptr<ClassCmdsImpl> impl_;
};
} // end of namespace class_cmds
} // end of namespace isc
#endif // CLASS_CMDS_H

View File

@ -0,0 +1,182 @@
// Copyright (C) 2018-2022 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/.
// 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.
#include <config.h>
#include <class_cmds.h>
#include <class_cmds_log.h>
#include <cc/command_interpreter.h>
#include <hooks/hooks.h>
#include <dhcpsrv/cfgmgr.h>
#include <process/daemon.h>
using namespace isc::config;
using namespace isc::data;
using namespace isc::dhcp;
using namespace isc::hooks;
using namespace isc::process;
using namespace isc::class_cmds;
extern "C" {
/// @brief This is a command callout for 'class-add' command.
///
/// @param handle Callout handle used to retrieve a command and
/// provide a response.
/// @return 0 if this callout has been invoked successfully,
/// 1 otherwise.
int class_add(CalloutHandle& handle) {
try {
ClassCmds class_cmds;
class_cmds.addClass(handle);
} catch (const std::exception& ex) {
LOG_ERROR(class_cmds_logger, CLASS_CMDS_CLASS_ADD_HANDLER_FAILED)
.arg(ex.what());
return (1);
}
return (0);
}
/// @brief This is a command callout for 'class-get' command.
///
/// @param handle Callout handle used to retrieve a command and
/// provide a response.
/// @return 0 if this callout has been invoked successfully,
/// 1 otherwise.
int class_get(CalloutHandle& handle) {
try {
ClassCmds class_cmds;
class_cmds.getClass(handle);
} catch (const std::exception& ex) {
LOG_ERROR(class_cmds_logger, CLASS_CMDS_CLASS_GET_HANDLER_FAILED)
.arg(ex.what());
return (1);
}
return (0);
}
/// @brief This is a command callout for 'class-list' command.
///
/// @param handle Callout handle used to retrieve a command and
/// provide a response.
/// @return 0 if this callout has been invoked successfully,
/// 1 otherwise.
int class_list(CalloutHandle& handle) {
try {
ClassCmds class_cmds;
class_cmds.getClassList(handle);
} catch (const std::exception& ex) {
LOG_ERROR(class_cmds_logger, CLASS_CMDS_CLASS_LIST_HANDLER_FAILED)
.arg(ex.what());
return (1);
}
return (0);
}
/// @brief This is a command callout for 'class-update' command.
///
/// @param handle Callout handle used to retrieve a command and
/// provide a response.
/// @return 0 if this callout has been invoked successfully,
/// 1 otherwise.
int class_update(CalloutHandle& handle) {
try {
ClassCmds class_cmds;
class_cmds.updateClass(handle);
} catch (const std::exception& ex) {
LOG_ERROR(class_cmds_logger, CLASS_CMDS_CLASS_UPDATE_HANDLER_FAILED)
.arg(ex.what());
return (1);
}
return (0);
}
/// @brief This is a command callout for 'class-del' command.
///
/// @param handle Callout handle used to retrieve a command and
/// provide a response.
/// @return 0 if this callout has been invoked successfully,
/// 1 otherwise.
int class_del(CalloutHandle& handle) {
try {
ClassCmds class_cmds;
class_cmds.delClass(handle);
} catch (const std::exception& ex) {
LOG_ERROR(class_cmds_logger, CLASS_CMDS_CLASS_DEL_HANDLER_FAILED)
.arg(ex.what());
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 not loadable by d2 or ca.
uint16_t family = CfgMgr::instance().getFamily();
const std::string& proc_name = Daemon::getProcName();
if (family == AF_INET) {
if (proc_name != "kea-dhcp4") {
isc_throw(isc::Unexpected, "Bad process name: " << proc_name
<< ", expected kea-dhcp4");
}
} else {
if (proc_name != "kea-dhcp6") {
isc_throw(isc::Unexpected, "Bad process name: " << proc_name
<< ", expected kea-dhcp6");
}
}
// Register commands.
handle.registerCommandCallout("class-add", class_add);
handle.registerCommandCallout("class-get", class_get);
handle.registerCommandCallout("class-list", class_list);
handle.registerCommandCallout("class-update", class_update);
handle.registerCommandCallout("class-del", class_del);
} catch (const std::exception& ex) {
LOG_ERROR(class_cmds_logger, CLASS_CMDS_INIT_FAILED)
.arg(ex.what());
return (1);
}
LOG_INFO(class_cmds_logger, CLASS_CMDS_INIT_OK);
return (0);
}
/// @brief This function is called when the library is unloaded.
///
/// @return 0
int unload() {
LOG_INFO(class_cmds_logger, CLASS_CMDS_DEINIT_OK);
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"

View File

@ -0,0 +1,18 @@
// Copyright (C) 2018-2022 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 <class_cmds_log.h>
namespace isc {
namespace class_cmds {
isc::log::Logger class_cmds_logger("class-cmds-hooks");
}
}

View File

@ -0,0 +1,23 @@
// Copyright (C) 2018-2022 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 CLASS_CMD_LOG_H
#define CLASS_CMD_LOG_H
#include <log/logger_support.h>
#include <log/macros.h>
#include <class_cmds_messages.h>
namespace isc {
namespace class_cmds {
extern isc::log::Logger class_cmds_logger;
} // end of isc::class_cmds
} // end of class_cmds namespace
#endif

View File

@ -0,0 +1,61 @@
// File created from ../../../../src/hooks/dhcp/class_cmds/class_cmds_messages.mes
#include <cstddef>
#include <log/message_types.h>
#include <log/message_initializer.h>
extern const isc::log::MessageID CLASS_CMDS_CLASS_ADD = "CLASS_CMDS_CLASS_ADD";
extern const isc::log::MessageID CLASS_CMDS_CLASS_ADD_FAILED = "CLASS_CMDS_CLASS_ADD_FAILED";
extern const isc::log::MessageID CLASS_CMDS_CLASS_ADD_HANDLER_FAILED = "CLASS_CMDS_CLASS_ADD_HANDLER_FAILED";
extern const isc::log::MessageID CLASS_CMDS_CLASS_DEL = "CLASS_CMDS_CLASS_DEL";
extern const isc::log::MessageID CLASS_CMDS_CLASS_DEL_EMPTY = "CLASS_CMDS_CLASS_DEL_EMPTY";
extern const isc::log::MessageID CLASS_CMDS_CLASS_DEL_FAILED = "CLASS_CMDS_CLASS_DEL_FAILED";
extern const isc::log::MessageID CLASS_CMDS_CLASS_DEL_HANDLER_FAILED = "CLASS_CMDS_CLASS_DEL_HANDLER_FAILED";
extern const isc::log::MessageID CLASS_CMDS_CLASS_GET = "CLASS_CMDS_CLASS_GET";
extern const isc::log::MessageID CLASS_CMDS_CLASS_GET_EMPTY = "CLASS_CMDS_CLASS_GET_EMPTY";
extern const isc::log::MessageID CLASS_CMDS_CLASS_GET_FAILED = "CLASS_CMDS_CLASS_GET_FAILED";
extern const isc::log::MessageID CLASS_CMDS_CLASS_GET_HANDLER_FAILED = "CLASS_CMDS_CLASS_GET_HANDLER_FAILED";
extern const isc::log::MessageID CLASS_CMDS_CLASS_LIST = "CLASS_CMDS_CLASS_LIST";
extern const isc::log::MessageID CLASS_CMDS_CLASS_LIST_EMPTY = "CLASS_CMDS_CLASS_LIST_EMPTY";
extern const isc::log::MessageID CLASS_CMDS_CLASS_LIST_FAILED = "CLASS_CMDS_CLASS_LIST_FAILED";
extern const isc::log::MessageID CLASS_CMDS_CLASS_LIST_HANDLER_FAILED = "CLASS_CMDS_CLASS_LIST_HANDLER_FAILED";
extern const isc::log::MessageID CLASS_CMDS_CLASS_UPDATE = "CLASS_CMDS_CLASS_UPDATE";
extern const isc::log::MessageID CLASS_CMDS_CLASS_UPDATE_EMPTY = "CLASS_CMDS_CLASS_UPDATE_EMPTY";
extern const isc::log::MessageID CLASS_CMDS_CLASS_UPDATE_FAILED = "CLASS_CMDS_CLASS_UPDATE_FAILED";
extern const isc::log::MessageID CLASS_CMDS_CLASS_UPDATE_HANDLER_FAILED = "CLASS_CMDS_CLASS_UPDATE_HANDLER_FAILED";
extern const isc::log::MessageID CLASS_CMDS_DEINIT_OK = "CLASS_CMDS_DEINIT_OK";
extern const isc::log::MessageID CLASS_CMDS_INIT_FAILED = "CLASS_CMDS_INIT_FAILED";
extern const isc::log::MessageID CLASS_CMDS_INIT_OK = "CLASS_CMDS_INIT_OK";
namespace {
const char* values[] = {
"CLASS_CMDS_CLASS_ADD", "class added: %1",
"CLASS_CMDS_CLASS_ADD_FAILED", "failed to add a new class: %1",
"CLASS_CMDS_CLASS_ADD_HANDLER_FAILED", "failed to run handler for 'class-add' command",
"CLASS_CMDS_CLASS_DEL", "class deleted: %1",
"CLASS_CMDS_CLASS_DEL_EMPTY", "class not deleted (not found): %1",
"CLASS_CMDS_CLASS_DEL_FAILED", "failed to delete a class: %1",
"CLASS_CMDS_CLASS_DEL_HANDLER_FAILED", "failed to run handler for 'class-del' command",
"CLASS_CMDS_CLASS_GET", "successfully retrieved a class: %1",
"CLASS_CMDS_CLASS_GET_EMPTY", "specified class was not found: %1",
"CLASS_CMDS_CLASS_GET_FAILED", "failed to retrieve a class: %1",
"CLASS_CMDS_CLASS_GET_HANDLER_FAILED", "failed to run handler for 'class-get' command",
"CLASS_CMDS_CLASS_LIST", "successfully retrieved classes names",
"CLASS_CMDS_CLASS_LIST_EMPTY", "no class was found",
"CLASS_CMDS_CLASS_LIST_FAILED", "failed to retrieve classes names: %1",
"CLASS_CMDS_CLASS_LIST_HANDLER_FAILED", "failed to run handler for 'class-list' command",
"CLASS_CMDS_CLASS_UPDATE", "class updated: %1",
"CLASS_CMDS_CLASS_UPDATE_EMPTY", "class not updated (not found): %1",
"CLASS_CMDS_CLASS_UPDATE_FAILED", "failed to update a class: %1",
"CLASS_CMDS_CLASS_UPDATE_HANDLER_FAILED", "failed to run handler for 'class-update' command",
"CLASS_CMDS_DEINIT_OK", "unloading Class Commands hooks library successful",
"CLASS_CMDS_INIT_FAILED", "loading Class Commands hooks library failed: %1",
"CLASS_CMDS_INIT_OK", "loading Class Commands hooks library successful",
NULL
};
const isc::log::MessageInitializer initializer(values);
} // Anonymous namespace

Some files were not shown because too many files have changed in this diff Show More