diff options
Diffstat (limited to 'src/hooks/dhcp/high_availability/query_filter.cc')
-rw-r--r-- | src/hooks/dhcp/high_availability/query_filter.cc | 500 |
1 files changed, 500 insertions, 0 deletions
diff --git a/src/hooks/dhcp/high_availability/query_filter.cc b/src/hooks/dhcp/high_availability/query_filter.cc new file mode 100644 index 0000000..b195858 --- /dev/null +++ b/src/hooks/dhcp/high_availability/query_filter.cc @@ -0,0 +1,500 @@ +// 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 <ha_log.h> +#include <query_filter.h> +#include <dhcp/dhcp4.h> +#include <dhcp/dhcp6.h> +#include <dhcp/option.h> +#include <exceptions/exceptions.h> +#include <util/multi_threading_mgr.h> + +#include <array> +#include <iostream> +#include <sstream> + +using namespace isc::dhcp; +using namespace isc::log; +using namespace isc::util; + +namespace { + +/// @brief A "mixing table" of 256 distinct values, in pseudo-random order. +/// +/// The mixing table comes from section 6 of RFC3074. +std::array<uint8_t, 256> loadb_mx_tbl = { { + 251, 175, 119, 215, 81, 14, 79, 191, 103, 49, 181, 143, 186, 157, 0, + 232, 31, 32, 55, 60, 152, 58, 17, 237, 174, 70, 160, 144, 220, 90, 57, + 223, 59, 3, 18, 140, 111, 166, 203, 196, 134, 243, 124, 95, 222, 179, + 197, 65, 180, 48, 36, 15, 107, 46, 233, 130, 165, 30, 123, 161, 209, 23, + 97, 16, 40, 91, 219, 61, 100, 10, 210, 109, 250, 127, 22, 138, 29, 108, + 244, 67, 207, 9, 178, 204, 74, 98, 126, 249, 167, 116, 34, 77, 193, + 200, 121, 5, 20, 113, 71, 35, 128, 13, 182, 94, 25, 226, 227, 199, 75, + 27, 41, 245, 230, 224, 43, 225, 177, 26, 155, 150, 212, 142, 218, 115, + 241, 73, 88, 105, 39, 114, 62, 255, 192, 201, 145, 214, 168, 158, 221, + 148, 154, 122, 12, 84, 82, 163, 44, 139, 228, 236, 205, 242, 217, 11, + 187, 146, 159, 64, 86, 239, 195, 42, 106, 198, 118, 112, 184, 172, 87, + 2, 173, 117, 176, 229, 247, 253, 137, 185, 99, 164, 102, 147, 45, 66, + 231, 52, 141, 211, 194, 206, 246, 238, 56, 110, 78, 248, 63, 240, 189, + 93, 92, 51, 53, 183, 19, 171, 72, 50, 33, 104, 101, 69, 8, 252, 83, 120, + 76, 135, 85, 54, 202, 125, 188, 213, 96, 235, 136, 208, 162, 129, 190, + 132, 156, 38, 47, 1, 7, 254, 24, 4, 216, 131, 89, 21, 28, 133, 37, 153, + 149, 80, 170, 68, 6, 169, 234, 151 } +}; + +/// @brief Table indicating which DHCPv4 message types are of interest to HA. +std::array<bool, DHCP_TYPES_EOF> v4_ha_types = { + false, // DHCP_NOTYPE = 0 + true, // DHCPDISCOVER = 1 + false, // DHCPOFFER = 2 + true, // DHCPREQUEST = 3 + true, // DHCPDECLINE = 4 + false, // DHCPACK = 5 + false, // DHCPNAK = 6 + true, // DHCPRELEASE = 7 + true, // DHCPINFORM = 8 + false, // DHCPFORCERENEW = 9 + false, // DHCPLEASEQUERY = 10 + false, // DHCPLEASEUNASSIGNED = 11 + false, // DHCPLEASEUNKNOWN = 12 + false, // DHCPLEASEACTIVE = 13 + false, // DHCPBULKLEASEQUERY = 14 + false, // DHCPLEASEQUERYDONE = 15 + false, // DHCPACTIVELEASEQUERY = 16 + false, // DHCPLEASEQUERYSTATUS = 17 + false // DHCPTLS = 18 +}; + +/// @brief Table indicating which DHCPv6 message types are of interest to HA. +std::array<bool, DHCPV6_TYPES_EOF> v6_ha_types = { + false, // DHCPV6_NOTYPE = 0 + true, // DHCPV6_SOLICIT = 1 + false, // DHCPV6_ADVERTISE = 2 + true, // DHCPV6_REQUEST = 3 + true, // DHCPV6_CONFIRM = 4 + true, // DHCPV6_RENEW = 5 + true, // DHCPV6_REBIND = 6 + false, // DHCPV6_REPLY = 7 + true, // DHCPV6_RELEASE = 8 + true, // DHCPV6_DECLINE = 9 + false, // DHCPV6_RECONFIGURE = 10 + false, // DHCPV6_INFORMATION_REQUEST = 11 + false, // DHCPV6_RELAY_FORW = 12 + false, // DHCPV6_RELAY_REPL = 13 + false, // DHCPV6_LEASEQUERY = 14 + false, // DHCPV6_LEASEQUERY_REPLY = 15 + false, // DHCPV6_LEASEQUERY_DONE = 16 + false, // DHCPV6_LEASEQUERY_DATA = 17 + false, // DHCPV6_RECONFIGURE_REQUEST = 18 + false, // DHCPV6_RECONFIGURE_REPLY = 19 + /// @todo There is some question as to whether DHCPV4_QUERY should get load-balanced + /// or not. It may not provide sufficient information to properly scope it. For now + /// we will not load-balance them. + false, // DHCPV6_DHCPV4_QUERY = 20 + false, // DHCPV6_DHCPV4_RESPONSE = 21 + false, // DHCPV6_ACTIVELEASEQUERY = 22 + false, // DHCPV6_STARTTLS = 23 + false, // DHCPV6_BNDUPD = 24 + false, // DHCPV6_BNDREPLY = 25 + false, // DHCPV6_POOLREQ = 26 + false, // DHCPV6_POOLRESP = 27 + false, // DHCPV6_UPDREQ = 28 + false, // DHCPV6_UPDREQALL = 29 + false, // DHCPV6_UPDDONE = 30 + false, // DHCPV6_CONNECT = 31 + false, // DHCPV6_CONNECTREPLY = 32 + false, // DHCPV6_DISCONNECT = 33 + false, // DHCPV6_STATE = 34 + false // DHCPV6_CONTACT = 35 +}; + +} // end of anonymous namespace + +namespace isc { +namespace ha { + +QueryFilter::QueryFilter(const HAConfigPtr& config) + : config_(config), peers_(), scopes_(), active_servers_(0), + mutex_(new std::mutex) { + + // Make sure that the configuration is valid. We make certain + // assumptions about the availability of the servers' configurations + // in the config_ structure. + config_->validate(); + + HAConfig::PeerConfigMap peers_map = config->getAllServersConfig(); + std::vector<HAConfig::PeerConfigPtr> backup_peers; + + // The returned configurations are not ordered. Let's iterate over them + // and put them in the desired order. + for (auto peer_pair = peers_map.begin(); peer_pair != peers_map.end(); ++peer_pair) { + auto peer = peer_pair->second; + // The primary server is always first on the list. + if (peer->getRole() == HAConfig::PeerConfig::PRIMARY) { + peers_.insert(peers_.begin(), peer); + ++active_servers_; + + // The secondary server is always behind the primary server. + } else if ((peer->getRole() == HAConfig::PeerConfig::SECONDARY) || + (peer->getRole() == HAConfig::PeerConfig::STANDBY)) { + peers_.push_back(peer); + + // If this is a secondary server, we're in the load balancing + // mode, in which case we have two active servers. + if (peer->getRole() == HAConfig::PeerConfig::SECONDARY) { + ++active_servers_; + } + + // If this is neither primary nor secondary/standby, it is a backup. + } else { + backup_peers.push_back(peer); + } + } + + // Append backup servers to the list. + if (!backup_peers.empty()) { + peers_.insert(peers_.end(), backup_peers.begin(), backup_peers.end()); + } + + // The query filter is initially setup to serve default scopes, i.e. for the + // load balancing case the primary and secondary are responsible for their + // own scopes. The backup servers are not responding to any queries. In the + // hot standby mode, the primary server is responsible for the entire traffic. + // The standby server is not responding. + serveDefaultScopes(); +} + +void +QueryFilter::serveScope(const std::string& scope_name) { + if (MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lock(*mutex_); + serveScopeInternal(scope_name); + } else { + serveScopeInternal(scope_name); + } +} + +void +QueryFilter::serveScopeInternal(const std::string& scope_name) { + validateScopeName(scope_name); + scopes_[scope_name] = true; +} + +void +QueryFilter::serveScopeOnly(const std::string& scope_name) { + if (MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lock(*mutex_); + serveScopeOnlyInternal(scope_name); + } else { + serveScopeOnlyInternal(scope_name); + } +} + +void +QueryFilter::serveScopeOnlyInternal(const std::string& scope_name) { + validateScopeName(scope_name); + serveNoScopesInternal(); + serveScopeInternal(scope_name); +} + +void +QueryFilter::serveScopes(const std::vector<std::string>& scopes) { + if (MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lock(*mutex_); + serveScopesInternal(scopes); + } else { + serveScopesInternal(scopes); + } +} + +void +QueryFilter::serveScopesInternal(const std::vector<std::string>& scopes) { + // Remember currently enabled scopes in case we fail to process + // the provided list of scopes. + auto current_scopes = scopes_; + try { + serveNoScopesInternal(); + for (size_t i = 0; i < scopes.size(); ++i) { + serveScopeInternal(scopes[i]); + } + + } catch (...) { + // There was an error processing scopes list. Need to revert + // to the previous configuration. + scopes_ = current_scopes; + throw; + } +} + +void +QueryFilter::serveDefaultScopes() { + if (MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lock(*mutex_); + serveDefaultScopesInternal(); + } else { + serveDefaultScopesInternal(); + } +} + +void +QueryFilter::serveDefaultScopesInternal() { + // Get this server instance configuration. + HAConfig::PeerConfigPtr my_config = config_->getThisServerConfig(); + HAConfig::PeerConfig::Role my_role = my_config->getRole(); + + // Clear scopes. + serveNoScopesInternal(); + + // If I am primary or secondary, then I am only responsible for my own + // scope. If I am standby, I am not responsible for any scope. + if ((my_role == HAConfig::PeerConfig::PRIMARY) || + (my_role == HAConfig::PeerConfig::SECONDARY)) { + serveScopeInternal(my_config->getName()); + } +} + +void +QueryFilter::serveFailoverScopes() { + if (MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lock(*mutex_); + serveFailoverScopesInternal(); + } else { + serveFailoverScopesInternal(); + } +} + +void +QueryFilter::serveFailoverScopesInternal() { + // Clear scopes. + serveNoScopesInternal(); + + // Iterate over the roles of all servers to see which scope should + // be enabled. + for (auto peer = peers_.begin(); peer != peers_.end(); ++peer) { + // The scope of the primary server must always be served. If we're + // doing load balancing, the scope of the secondary server also + // has to be served. Regardless if I am primary or secondary, + // I will start serving queries from both scopes. If I am a + // standby server, I will start serving the scope of the primary + // server. + if (((*peer)->getRole() == HAConfig::PeerConfig::PRIMARY) || + ((*peer)->getRole() == HAConfig::PeerConfig::SECONDARY)) { + serveScopeInternal((*peer)->getName()); + } + } +} + +void +QueryFilter::serveNoScopes() { + if (MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lock(*mutex_); + serveNoScopesInternal(); + } else { + serveNoScopesInternal(); + } +} + +void +QueryFilter::serveNoScopesInternal() { + scopes_.clear(); + + // Disable scope for each peer in the configuration. + for (auto peer = peers_.begin(); peer != peers_.end(); ++peer) { + scopes_[(*peer)->getName()] = false; + } +} + +bool +QueryFilter::amServingScope(const std::string& scope_name) const { + if (MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lock(*mutex_); + return (amServingScopeInternal(scope_name)); + } else { + return (amServingScopeInternal(scope_name)); + } +} + +bool +QueryFilter::amServingScopeInternal(const std::string& scope_name) const { + auto scope = scopes_.find(scope_name); + return ((scope == scopes_.end()) || (scope->second)); +} + +std::set<std::string> +QueryFilter::getServedScopes() const { + if (MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lock(*mutex_); + return (getServedScopesInternal()); + } else { + return (getServedScopesInternal()); + } +} + +std::set<std::string> +QueryFilter::getServedScopesInternal() const { + std::set<std::string> scope_set; + for (auto scope : scopes_) { + if (scope.second) { + scope_set.insert(scope.first); + } + } + return (scope_set); +} + +bool +QueryFilter::isHaType(const dhcp::Pkt4Ptr& query4) { + auto msg_type = query4->getType(); + return (msg_type < v4_ha_types.size() && v4_ha_types[msg_type]); +} + +bool +QueryFilter::isHaType(const dhcp::Pkt6Ptr& query) { + auto msg_type = query->getType(); + return (msg_type < v6_ha_types.size() && v6_ha_types[msg_type]); +} + + +bool +QueryFilter::inScope(const dhcp::Pkt4Ptr& query4, std::string& scope_class) const { + if (MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lock(*mutex_); + return (inScopeInternal(query4, scope_class)); + } else { + return (inScopeInternal(query4, scope_class)); + } +} + +bool +QueryFilter::inScope(const dhcp::Pkt6Ptr& query6, std::string& scope_class) const { + if (MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lock(*mutex_); + return (inScopeInternal(query6, scope_class)); + } else { + return (inScopeInternal(query6, scope_class)); + } +} + +template<typename QueryPtrType> +bool +QueryFilter::inScopeInternal(const QueryPtrType& query, + std::string& scope_class) const { + if (!query) { + isc_throw(BadValue, "query must not be null"); + } + + + // If it's not a type HA cares about, it's in scope for this peer. + if (!isHaType(query)) { + auto scope = peers_[0]->getName(); + scope_class = makeScopeClass(scope); + return (true); + } + + int candidate_server = 0; + + // If we're doing load balancing we have to check if this query + // belongs to us or the partner. If it belongs to a partner but + // we're configured to serve this scope, we should accept it. + if (config_->getHAMode() == HAConfig::LOAD_BALANCING) { + candidate_server = loadBalance(query); + // Malformed query received. + if (candidate_server < 0) { + return (false); + } + } + + auto scope = peers_[candidate_server]->getName(); + scope_class = makeScopeClass(scope); + return ((candidate_server >= 0) && amServingScopeInternal(scope)); +} + +int +QueryFilter::loadBalance(const dhcp::Pkt4Ptr& query4) const { + uint8_t lb_hash = 0; + // Try to compute the hash by client identifier if the client + // identifier has been specified. + OptionPtr opt_client_id = query4->getOption(DHO_DHCP_CLIENT_IDENTIFIER); + if (opt_client_id && !opt_client_id->getData().empty()) { + const auto& client_id_key = opt_client_id->getData(); + lb_hash = loadBalanceHash(&client_id_key[0], client_id_key.size()); + + } else { + // No client identifier available. Use the HW address instead. + HWAddrPtr hwaddr = query4->getHWAddr(); + if (hwaddr && !hwaddr->hwaddr_.empty()) { + lb_hash = loadBalanceHash(&hwaddr->hwaddr_[0], hwaddr->hwaddr_.size()); + + } else { + // No client identifier and no HW address. Indicate an + // error. + std::stringstream xid; + xid << "0x" << std::hex << query4->getTransid() << std::dec; + LOG_DEBUG(ha_logger, DBGLVL_TRACE_BASIC, HA_LOAD_BALANCING_IDENTIFIER_MISSING) + .arg(xid.str()); + return (-1); + } + } + + // The hash value modulo number of active servers gives an index + // of the server to process the packet. + return (active_servers_ > 0 ? static_cast<int>(lb_hash % active_servers_) : -1); +} + +int +QueryFilter::loadBalance(const dhcp::Pkt6Ptr& query6) const { + uint8_t lb_hash = 0; + // Compute the hash by DUID if the DUID. + OptionPtr opt_duid = query6->getOption(D6O_CLIENTID); + if (opt_duid && !opt_duid->getData().empty()) { + const auto& duid_key = opt_duid->getData(); + lb_hash = loadBalanceHash(&duid_key[0], duid_key.size()); + + } else { + // No DUID. Indicate an error. + std::stringstream xid; + xid << "0x" << std::hex << query6->getTransid() << std::dec; + LOG_DEBUG(ha_logger, DBGLVL_TRACE_BASIC, HA_LOAD_BALANCING_DUID_MISSING) + .arg(xid.str()); + return (-1); + } + + // The hash value modulo number of active servers gives an index + // of the server to process the packet. + return (active_servers_ > 0 ? static_cast<int>(lb_hash % active_servers_) : -1); +} + +uint8_t +QueryFilter::loadBalanceHash(const uint8_t* key, const size_t key_len) const { + uint8_t hash = static_cast<uint8_t>(key_len); + + for (auto i = key_len; i > 0;) { + hash = loadb_mx_tbl[hash ^ key[--i]]; + } + + return (hash); +} + +void +QueryFilter::validateScopeName(const std::string& scope_name) const { + try { + // If there is no such server, the scope name is invalid. + static_cast<void>(config_->getPeerConfig(scope_name)); + + } catch (...) { + isc_throw(BadValue, "invalid server name specified '" << scope_name + << "' while enabling/disabling HA scopes"); + } +} + +std::string +QueryFilter::makeScopeClass(const std::string& scope_name) const { + return (std::string("HA_") + scope_name); +} + +} // end of namespace isc::ha +} // end of namespace isc |