diff options
Diffstat (limited to 'src/lib/dhcpsrv/d2_client_mgr.h')
-rw-r--r-- | src/lib/dhcpsrv/d2_client_mgr.h | 530 |
1 files changed, 530 insertions, 0 deletions
diff --git a/src/lib/dhcpsrv/d2_client_mgr.h b/src/lib/dhcpsrv/d2_client_mgr.h new file mode 100644 index 0000000..7b282c5 --- /dev/null +++ b/src/lib/dhcpsrv/d2_client_mgr.h @@ -0,0 +1,530 @@ +// Copyright (C) 2014-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 D2_CLIENT_MGR_H +#define D2_CLIENT_MGR_H + +/// @file d2_client_mgr.h Defines the D2ClientMgr class. +/// This file defines the class Kea uses to act as a client of the +/// kea-dhcp-ddns module (aka D2). +/// +#include <asiolink/io_address.h> +#include <dhcp_ddns/ncr_io.h> +#include <dhcpsrv/d2_client_cfg.h> +#include <dhcpsrv/srv_config.h> +#include <exceptions/exceptions.h> + +#include <boost/algorithm/string.hpp> +#include <boost/shared_ptr.hpp> +#include <boost/noncopyable.hpp> + +#include <stdint.h> +#include <string> +#include <vector> +#include <sstream> + +namespace isc { +namespace dhcp { + +/// @brief Defines the type for D2 IO error handler. +/// This callback is invoked when a send to kea-dhcp-ddns completes with a +/// failed status. This provides the application layer (Kea) with a means to +/// handle the error appropriately. +/// +/// @param result Result code of the send operation. +/// @param ncr NameChangeRequest which failed to send. +/// +/// @note Handlers are expected not to throw. In the event a handler does +/// throw invoking code logs the exception and then swallows it. +typedef +std::function<void(const dhcp_ddns::NameChangeSender::Result result, + dhcp_ddns::NameChangeRequestPtr& ncr)> D2ClientErrorHandler; + +/// @brief D2ClientMgr isolates Kea from the details of being a D2 client. +/// +/// Provides services for managing the current dhcp-ddns configuration and +/// as well as communications with kea-dhcp-ddns. Regarding configuration it +/// provides services to store, update, and access the current dhcp-ddns +/// configuration. As for kea-dhcp-ddns communications, D2ClientMgr creates +/// maintains a NameChangeSender appropriate to the current configuration and +/// provides services to start, stop, and post NCRs to the sender. Additionally +/// there are methods to examine the queue of requests currently waiting for +/// transmission. +/// +/// The manager also provides the mechanics to integrate the ASIO-based IO +/// used by the NCR IPC with the select-driven IO used by Kea. Senders expose +/// a file descriptor, the "select-fd" that can monitored for read-readiness +/// with the select() function (or variants). D2ClientMgr provides a method, +/// runReadyIO(), that will instructs the sender to process the next ready +/// ready IO handler on the sender's IOservice. Track# 3315 extended +/// Kea's IfaceMgr to support the registration of multiple external sockets +/// with callbacks that are then monitored with IO readiness via select(). +/// D2ClientMgr registers the sender's select-fd and runReadyIO() with +/// IfaceMgr when entering the send mode and unregisters it when exiting send +/// mode. +/// +/// To place the manager in send mode, the calling layer must supply an error +/// handler and optionally an IOService instance. The error handler is invoked +/// if a send completes with a failed status. This provides the calling layer +/// an opportunity act upon the error. +/// +/// If the caller supplies an IOService, that service will be used to process +/// the sender's IO. If not supplied, D2ClientMgr pass a private IOService +/// into the sender. Using a private service isolates the sender's IO from +/// any other services. +/// +class D2ClientMgr : public dhcp_ddns::NameChangeSender::RequestSendHandler, + boost::noncopyable { +public: + /// @brief Constructor + /// + /// Default constructor which constructs an instance which has DHCP-DDNS + /// updates disabled. + D2ClientMgr(); + + /// @brief Destructor. + ~D2ClientMgr(); + + /// @brief Updates the DHCP-DDNS client configuration to the given value. + /// + /// @param new_config pointer to the new client configuration. + /// @throw D2ClientError if passed an empty pointer. + void setD2ClientConfig(D2ClientConfigPtr& new_config); + + /// @brief Convenience method for checking if DHCP-DDNS is enabled. + /// + /// @return True if the D2 configuration is enabled. + bool ddnsEnabled(); + + /// @brief Fetches the DHCP-DDNS configuration pointer. + /// + /// @return a reference to the current configuration pointer. + const D2ClientConfigPtr& getD2ClientConfig() const; + + /// @brief Determines server flags based on configuration and client flags. + /// + /// This method uses input values for the client's FQDN S and N flags, in + /// conjunction with the configuration parameters updates-enabled, override- + /// no-updates, and override-client-updates to determine the values that + /// should be used for the server's FQDN S and N flags. + /// The logic in this method is based upon RFCs 4702 and 4704, and is + /// shown in the following truth table: + /// + /// @code + /// + /// When Updates are enabled: + /// + /// ON = Override No Updates, OC = Override Client Updates + /// + /// | Client |-------- Server Response Flags ------------| + /// | Flags | ON=F,OC=F | ON=F,OC=T | ON=T,OC=F | ON=T,OC=T | + /// | N-S | N-S-O | N-S-O | N-S-O | N-S-O | + /// ---------------------------------------------------------- + /// | 0-0 | 0-0-0 | 0-1-1 | 0-0-0 | 0-1-1 | + /// | 0-1 | 0-1-0 | 0-1-0 | 0-1-0 | 0-1-0 | + /// | 1-0 | 1-0-0 | 1-0-0 | 0-1-1 | 0-1-1 | + /// + /// One can then use the server response flags to know when forward and + /// reverse updates should be performed: + /// + /// - Forward updates should be done when the Server S-Flag is true. + /// - Reverse updates should be done when the Server N-Flag is false. + /// + /// When Updates are disabled: + /// + /// | Client | Server | + /// | N-S | N-S-O | + /// -------------------- + /// | 0-0 | 1-0-0 | + /// | 0-1 | 1-0-1 | + /// | 1-0 | 1-0-0 | + /// + /// @endcode + /// + /// @param client_s S Flag from the client's FQDN + /// @param client_n N Flag from the client's FQDN + /// @param server_s [out] S Flag for the server's FQDN + /// @param server_n [out] N Flag for the server's FQDN + /// @param ddns_params DDNS behavioral configuration parameters + /// + /// @throw isc::BadValue if client_s and client_n are both 1 as this is + /// an invalid combination per RFCs. + void analyzeFqdn(const bool client_s, const bool client_n, bool& server_s, + bool& server_n, const DdnsParams& ddns_params) const; + + /// @brief Builds a FQDN based on the configuration and given IP address. + /// + /// Using the current values for generated-prefix, qualifying-suffix and + /// an IP address, this method constructs a fully qualified domain name. + /// It supports both IPv4 and IPv6 addresses. The format of the name + /// is as follows: + /// + /// <generated-prefix>-<ip address>.<qualifying-suffix>. + /// + /// <ip-address> is the result of IOAddress.toText() with the delimiters + /// ('.' for IPv4 or ':' for IPv6) replaced with a hyphen, '-'. + /// + /// @param address IP address from which to derive the name (IPv4 or IPv6) + /// @param ddns_params DDNS behavioral configuration parameters + /// @param trailing_dot A boolean value which indicates whether trailing + /// dot should be appended (if true) or not (false). + /// + /// @return std::string containing the generated name. + std::string generateFqdn(const asiolink::IOAddress& address, + const DdnsParams& ddns_params, + const bool trailing_dot = true) const; + + /// @brief Adds a qualifying suffix to a given domain name + /// + /// Constructs a FQDN based on the configured qualifying-suffix and + /// a partial domain name as follows: + /// + /// <partial_name>.<qualifying-suffix>. + /// + /// Note that the qualifying suffix will only be appended if the + /// input name does not already end with that suffix. + /// + /// @param partial_name domain name to qualify + /// @param ddns_params DDNS behavioral configuration parameters + /// @param trailing_dot A boolean value which when true guarantees the + /// result will end with a "." and when false that the result will not + /// end with a "." Note that this rule is applied even if the qualifying + /// suffix itself is empty (i.e. ""). + /// + /// @return std::string containing the qualified name. + std::string qualifyName(const std::string& partial_name, + const DdnsParams& ddns_params, + const bool trailing_dot) const; + + /// @brief Set server FQDN flags based on configuration and a given FQDN + /// + /// Templated wrapper around the analyzeFqdn() allowing that method to + /// be used for either IPv4 or IPv6 processing. This methods resets all + /// of the flags in the response to zero and then sets the S,N, and O + /// flags. Any other flags are the responsibility of the invoking layer. + /// + /// @param fqdn FQDN option from which to read client (inbound) flags + /// @param fqdn_resp FQDN option to update with the server (outbound) flags + /// @param ddns_params DDNS behavioral configuration parameters + /// @tparam T FQDN Option class containing the FQDN data such as + /// dhcp::Option4ClientFqdn or dhcp::Option6ClientFqdn + template <class T> + void adjustFqdnFlags(const T& fqdn, T& fqdn_resp, + const DdnsParams& ddns_params); + + /// @brief Get directional update flags based on server FQDN flags + /// + /// Templated convenience method which determines whether forward and + /// reverse updates should be performed based on a server response version + /// of the FQDN flags. The logic is straight forward and currently not + /// dependent upon configuration specific values: + /// + /// * forward will be true if S_FLAG is true + /// * reverse will be true if N_FLAG is false + /// + /// @param fqdn_resp FQDN option from which to read server (outbound) flags + /// @param [out] forward bool value will be set to true if forward updates + /// should be done, false if not. + /// @param [out] reverse bool value will be set to true if reverse updates + /// should be done, false if not. + /// @tparam T FQDN Option class containing the FQDN data such as + /// dhcp::Option4ClientFqdn or dhcp::Option6ClientFqdn + template <class T> + void getUpdateDirections(const T& fqdn_resp, bool& forward, bool& reverse); + + /// @brief Set server FQDN name based on configuration and a given FQDN + /// + /// Templated method which adjusts the domain name value and type in + /// a server FQDN from a client (inbound) FQDN and the current + /// configuration. The logic is as follows: + /// + /// If replace-client-name is true or the supplied name is empty, the + /// server FQDN is set to ""/PARTIAL. + /// + /// If replace-client-name is false and the supplied name is a partial + /// name the server FQDN is set to the supplied name qualified by + /// appending the qualifying-suffix. + /// + /// If replace-client-name is false and the supplied name is a fully + /// qualified name, set the server FQDN to the supplied name. + /// + /// If hostname-char-set is not empty, the inbound name will be + /// sanitized. This is done by iterating over the domain name labels, + /// sanitizing each individually, and then concatenating them into a + /// new sanitized name. It is done this way to guard against the case + /// where the hostname-char-set does not protect dots from replacement. + /// + /// @param fqdn FQDN option from which to get client (inbound) name + /// @param fqdn_resp FQDN option to update with the adjusted name + /// @param ddns_params DDNS behavioral configuration parameters + /// @tparam T FQDN Option class containing the FQDN data such as + /// dhcp::Option4ClientFqdn or dhcp::Option6ClientFqdn + template <class T> + void adjustDomainName(const T& fqdn, T& fqdn_resp, + const DdnsParams& ddns_params); + + /// @brief Enables sending NameChangeRequests to kea-dhcp-ddns + /// + /// Places the NameChangeSender into send mode. This instructs the + /// sender to begin dequeuing and transmitting requests and to accept + /// additional requests via the sendRequest() method. + /// + /// @param error_handler application level error handler to cope with + /// sends that complete with a failed status. A valid function must be + /// supplied as the manager cannot know how an application should deal + /// with send failures. + /// @param io_service IOService to be used for sender IO event processing + /// @warning It is up to the invoking layer to ensure the io_service + /// instance used outlives the D2ClientMgr send mode. When the send mode + /// is exited, either explicitly by calling stopSender() or implicitly + /// through D2ClientMgr destruction, any ASIO objects such as sockets or + /// timers will be closed and released. If the io_service goes out of scope + /// first this behavior could be unpredictable. + /// + /// @throw D2ClientError if sender instance is null. Underlying layer + /// may throw NCRSenderExceptions exceptions. + void startSender(D2ClientErrorHandler error_handler, + isc::asiolink::IOService& io_service); + + /// @brief Enables sending NameChangeRequests to kea-dhcp-ddns + /// + /// Places the NameChangeSender into send mode. This instructs the + /// sender to begin dequeuing and transmitting requests and to accept + /// additional requests via the sendRequest() method. The manager + /// will create a new, private instance of an IOService for the sender + /// to use for IO event processing. + /// + /// @param error_handler application level error handler to cope with + /// sends that complete with a failed status. A valid function must be + /// supplied as the manager cannot know how an application should deal + /// with send failures. + /// + /// @throw D2ClientError if sender instance is null. Underlying layer + /// may throw NCRSenderExceptions exceptions. + void startSender(D2ClientErrorHandler error_handler); + + /// @brief Returns true if the sender is in send mode, false otherwise. + /// + /// A true value indicates that the sender is present and in accepting + /// messages for transmission, false otherwise. + bool amSending() const; + + /// @brief Disables sending NameChangeRequests to kea-dhcp-ddns + /// + /// Takes the NameChangeSender out of send mode. The sender will stop + /// transmitting requests, though any queued requests remain queued. + /// Attempts to queue additional requests via sendRequest will fail. + /// + /// @throw D2ClientError if sender instance is null. Underlying layer + /// may throw NCRSenderExceptions exceptions. + void stopSender(); + + /// @brief Send the given NameChangeRequests to kea-dhcp-ddns + /// + /// Passes NameChangeRequests to the NCR sender for transmission to + /// kea-dhcp-ddns. If the sender rejects the message, the client's error + /// handler will be invoked. The most likely cause for rejection is + /// the senders' queue has reached maximum capacity. + /// + /// @param ncr NameChangeRequest to send + /// + /// @throw D2ClientError if sender instance is null or not in send + /// mode. Either of these represents a programmatic error. + void sendRequest(dhcp_ddns::NameChangeRequestPtr& ncr); + + /// @brief Calls the client's error handler. + /// + /// Calls the error handler method set by startSender() when an + /// error occurs attempting to send a method. If the error handler + /// throws an exception it will be caught and logged. + /// + /// @param result contains that send outcome status. + /// @param ncr is a pointer to the NameChangeRequest that was attempted. + /// + /// This method is exception safe. + void invokeClientErrorHandler(const dhcp_ddns::NameChangeSender:: + Result result, + dhcp_ddns::NameChangeRequestPtr& ncr); + + /// @brief Returns the number of NCRs queued for transmission. + size_t getQueueSize() const; + + /// @brief Returns the maximum number of NCRs allowed in the queue. + size_t getQueueMaxSize() const; + + /// @brief Returns the nth NCR queued for transmission. + /// + /// Note that the entry is not removed from the queue. + /// @param index the index of the entry in the queue to fetch. + /// Valid values are 0 (front of the queue) to (queue size - 1). + /// @note This method is for test purposes only. + /// + /// @return Pointer reference to the queue entry. + /// + /// @throw D2ClientError if sender instance is null. Underlying layer + /// may throw NCRSenderExceptions exceptions. + const dhcp_ddns::NameChangeRequestPtr& peekAt(const size_t index) const; + + /// @brief Removes all NCRs queued for transmission. + /// + /// @throw D2ClientError if sender instance is null. Underlying layer + /// may throw NCRSenderExceptions exceptions. + void clearQueue(); + + /// @brief Processes sender IO events + /// + /// Serves as callback registered for the sender's select-fd with IfaceMgr. + /// It instructs the sender to execute the next ready IO handler. + /// It provides an instance method that can be bound via std::bind, as + /// NameChangeSender is abstract. + void runReadyIO(); + + /// @brief Suspends sending requests. + /// + /// This method is intended to be used when IO errors occur. It toggles + /// the enable-updates configuration flag to off, and takes the sender + /// out of send mode. Messages in the sender's queue will remain in the + /// queue. + /// @todo This logic may change in NameChangeSender is altered allow + /// queuing while stopped. Currently when a sender is not in send mode + /// it will not accept additional messages. + void suspendUpdates(); + +protected: + /// @brief Function operator implementing the NCR sender callback. + /// + /// This method is invoked each time the NameChangeSender completes + /// an asynchronous send. + /// + /// @param result contains that send outcome status. + /// @param ncr is a pointer to the NameChangeRequest that was + /// delivered (or attempted). + /// + /// @throw This method MUST NOT throw. + virtual void operator ()(const dhcp_ddns::NameChangeSender::Result result, + dhcp_ddns::NameChangeRequestPtr& ncr); + + /// @brief Fetches the sender's select-fd. + /// + /// The select-fd may be used with select() or poll(). If the sender has + /// IO waiting to process, the fd will evaluate as !EWOULDBLOCK. + /// @note This is only exposed for testing purposes. + /// + /// @return The sender's select-fd + /// + /// @throw D2ClientError if the sender does not exist or is not in send + /// mode. + int getSelectFd(); + + /// @brief Fetches the select-fd that is currently registered. + /// + /// @return The currently registered select-fd or + /// util::WatchSocket::SOCKET_NOT_VALID. + /// + /// @note This is only exposed for testing purposes. + int getRegisteredSelectFd(); + +private: + /// @brief Container class for DHCP-DDNS configuration parameters. + D2ClientConfigPtr d2_client_config_; + + /// @brief Pointer to the current interface to DHCP-DDNS. + dhcp_ddns::NameChangeSenderPtr name_change_sender_; + + /// @brief Private IOService to use if calling layer doesn't wish to + /// supply one. + boost::shared_ptr<asiolink::IOService> private_io_service_; + + /// @brief Application supplied error handler invoked when a send + /// completes with a failed status. + D2ClientErrorHandler client_error_handler_; + + /// @brief Remembers the select-fd registered with IfaceMgr. + int registered_select_fd_; +}; + +template <class T> +void +D2ClientMgr::adjustFqdnFlags(const T& fqdn, T& fqdn_resp, const DdnsParams& ddns_params) { + bool server_s = false; + bool server_n = false; + analyzeFqdn(fqdn.getFlag(T::FLAG_S), fqdn.getFlag(T::FLAG_N), + server_s, server_n, ddns_params); + + // Reset the flags to zero to avoid triggering N and S both 1 check. + fqdn_resp.resetFlags(); + + // Set S and N flags. + fqdn_resp.setFlag(T::FLAG_S, server_s); + fqdn_resp.setFlag(T::FLAG_N, server_n); + + // Set O flag true if server S overrides client S. + fqdn_resp.setFlag(T::FLAG_O, (fqdn.getFlag(T::FLAG_S) != server_s)); +} + +template <class T> +void +D2ClientMgr::getUpdateDirections(const T& fqdn_resp, + bool& forward, bool& reverse) { + forward = fqdn_resp.getFlag(T::FLAG_S); + reverse = !(fqdn_resp.getFlag(T::FLAG_N)); +} + +template <class T> +void +D2ClientMgr::adjustDomainName(const T& fqdn, T& fqdn_resp, const DdnsParams& ddns_params) { + // If we're configured to replace it or the supplied name is blank + // set the response name to blank. + D2ClientConfig::ReplaceClientNameMode mode = ddns_params.getReplaceClientNameMode(); + if ((mode == D2ClientConfig::RCM_ALWAYS || mode == D2ClientConfig::RCM_WHEN_PRESENT) || + fqdn.getDomainName().empty()) { + fqdn_resp.setDomainName("", T::PARTIAL); + } else { + // Sanitize the name the client sent us, if we're configured to do so. + std::string client_name = fqdn.getDomainName(); + + isc::util::str::StringSanitizerPtr sanitizer = ddns_params.getHostnameSanitizer(); + if (sanitizer) { + // We need the raw text form, so we can replace escaped chars + dns::Name tmp(client_name); + std::string raw_name = tmp.toRawText(); + + // We do not know if the sanitizer's regexp preserves dots, so + // we'll scrub it label by label. Yeah, lucky us. + // Using boost::split is simpler than using dns::Name::split() as + // that returns Names which have trailing dots etc. + std::vector<std::string> labels; + boost::algorithm::split(labels, raw_name, boost::is_any_of(".")); + std::stringstream ss; + for (auto label = labels.begin(); label != labels.end(); ++label ) { + if (label != labels.begin()) { + ss << "."; + } + + ss << sanitizer->scrub(*label); + } + + client_name = ss.str(); + } + + // If the supplied name is partial, qualify it by adding the suffix. + if (fqdn.getDomainNameType() == T::PARTIAL) { + fqdn_resp.setDomainName(qualifyName(client_name, ddns_params, true), T::FULL); + } else { + fqdn_resp.setDomainName(client_name, T::FULL); + } + } +} + + +/// @brief Defines a pointer for D2ClientMgr instances. +typedef boost::shared_ptr<D2ClientMgr> D2ClientMgrPtr; + + +} // namespace isc +} // namespace dhcp + +#endif |