.. note::
- For security purposes, there is no way to specify wildcards. Each requester address
- must be explicitly listed.
+ As of Kea 3.1.4, it is also possible to include ranges of requester addresses in CIDR
+ format such as "192.0.1.0/24" or "2001:db8:1::/64". Please note that specifying ranges
+ may be less secure than only using explicit IP addresses.
.. _lease-query-dhcpv6:
return (addresses_.count(address));
}
+void
+PoolSet::insert(const isc::asiolink::IOAddress& prefix, uint8_t prefix_len) {
+ PoolPtr pool;
+ if (getFamily() == AF_INET) {
+ pool = Pool4::create(prefix, prefix_len);
+ } else {
+ pool = Pool6::create(Lease::TYPE_NA, prefix, prefix_len);
+ }
+
+ if (pools_.find(pool) != pools_.end()) {
+ isc_throw(BadValue, "entry already exists");
+ }
+
+ pools_.emplace(pool);
+}
+
+bool
+PoolSet::contains(const IOAddress& address) const {
+ if (address.getFamily() != family_) {
+ isc_throw(BadValue, "not a " << (family_ == AF_INET ? "IPv4" : "IPv6")
+ << " address");
+ }
+
+ for (auto pool : pools_) {
+ if (pool->inRange(address)) {
+ return (true);
+ }
+ }
+
+ return (false);
+}
+
const SimpleKeywords
LeaseQueryImpl::LEASE_QUERY_KEYWORDS =
{
LeaseQueryImpl::LeaseQueryImpl(uint16_t family,
const ConstElementPtr config)
- : io_service_(new IOService()), address_list_(family) {
+ : io_service_(new IOService()), address_list_(family), pool_set_(family) {
if (!config || (config->getType() != Element::map)) {
isc_throw(BadValue, "Lease Query config is empty or not a map");
}
- ConstElementPtr requesters = config->get("requesters");
+ parserRequesters(config-get("requesters"));
+
+ ConstElementPtr advanced = config->get("advanced");
+ if (advanced) {
+ BulkLeaseQueryService::create(this, advanced);
+ }
+}
+
+LeaseQueryImpl::~LeaseQueryImpl() {
+ io_service_->stopAndPoll();
+}
+
+void
+LeaseQueryImpl::parserRequesters(ConstElementPtr requesters) {
if (!requesters || (requesters->getType() != Element::list)) {
isc_throw(BadValue,
"'requesters' address list is missing or not a list");
}
for (auto const& address_elem : requesters->listValue()) {
- try {
- IOAddress address(address_elem->stringValue());
- address_list_.insert(address);
- } catch (const std::exception& ex) {
- isc_throw(BadValue,
- "'requesters' entry '" << address_elem->stringValue()
+ auto entry_txt = address_elem->stringValue();
+ // first let's remove any whitespaces
+ boost::erase_all(entry_txt, " "); // space
+ boost::erase_all(entry_txt, "\t"); // tabulation
+
+ // Is this just an address or is it CIDR?
+ size_t pos = entry_txt.find("/");
+ if (pos == std::string::npos) {
+ try {
+ IOAddress address(entry_txt);
+ address_list_.insert(address);
+ } catch (const std::exception& ex) {
+ isc_throw(BadValue,
+ "'requesters' address entry '" << address_elem->stringValue()
<< "' is invalid: " << ex.what());
+ }
+ } else {
+ try {
+ IOAddress prefix = IOAddress(entry_txt.substr(0, pos));
+
+ // start with the first character after /
+ auto len_txt = entry_txt.substr(pos + 1);
+ int prefix_len = boost::lexical_cast<int>(len_txt);
+ if ((prefix_len < std::numeric_limits<uint8_t>::min()) ||
+ (prefix_len > std::numeric_limits<uint8_t>::max())) {
+ // This exception will be handled 4 line later!
+ isc_throw(OutOfRange, "prefix length " << len_txt << " is out of range");
+ }
+
+ pool_set_.insert(prefix, prefix_len);
+ } catch (const std::exception& ex) {
+ isc_throw(BadValue,
+ "'requesters' CIDR entry '" << entry_txt
+ << "' is invalid: " << ex.what());
+ }
}
}
- if (address_list_.size() == 0) {
- isc_throw(BadValue, "'requesters' address list cannot be empty");
+ if (address_list_.size() == 0 && pool_set_.size() == 0) {
+ isc_throw(BadValue, "'requesters' list cannot be empty");
}
-
- ConstElementPtr advanced = config->get("advanced");
- if (advanced) {
- BulkLeaseQueryService::create(this, advanced);
- }
-}
-
-LeaseQueryImpl::~LeaseQueryImpl() {
- io_service_->stopAndPoll();
}
bool
LeaseQueryImpl::isRequester(const IOAddress& address) const {
- return (address_list_.contains(address));
+ if (address_list_.contains(address)) {
+ return (true);
+ }
+
+ return (pool_set_.contains(address));
}
bool
#include <asiolink/io_address.h>
#include <asiolink/io_service.h>
#include <dhcp/pkt.h>
+#include <dhcpsrv/pool.h>
#include <cc/data.h>
#include <cc/simple_parser.h>
std::unordered_set<asiolink::IOAddress, boost::hash<asiolink::IOAddress> > addresses_;
};
+/// @brief Hash for a Pool based on it's address range.
+struct PoolRangeHash {
+ std::size_t operator()(const isc::dhcp::PoolPtr& p) const noexcept {
+ const auto& f = p->getFirstAddress();
+ const auto& l = p->getLastAddress();
+
+ isc::asiolink::IOAddress::Hash haddr;
+ std::size_t h1 = haddr(f);
+ std::size_t h2 = haddr(l);
+
+ // hash_combine
+ return h1 ^ (h2 + 0x9e3779b97f4a7c15ULL + (h1 << 6) + (h1 >> 2));
+ }
+};
+
+/// @brief Equality comparator for two pools based on their address range.
+struct PoolRangeEqual {
+ bool operator()(const isc::dhcp::PoolPtr& a,
+ const isc::dhcp::PoolPtr& b) const noexcept {
+ return a->getFirstAddress() == b->getFirstAddress() &&
+ a->getLastAddress() == b->getLastAddress();
+ }
+};
+
+/// @brief Defines an alias for a set of pools hashed by range.
+using PoolRangeSet = std::unordered_set<isc::dhcp::PoolPtr, PoolRangeHash, PoolRangeEqual>;
+
+/// @brief Manages a unique set of Pools of a given protocol family.
+/// The pools are hashed by their address range.
+class PoolSet {
+public:
+ /// @brief Constructor
+ ///
+ /// @param family protocol family of the set (AF_INET or AF_INET6)
+ PoolSet(uint16_t family)
+ : family_(family) { };
+
+ /// @brief Inserts an pool into the set.
+ ///
+ /// Creates a pool and adds it to the set, assuming it is not
+ /// already in the set.
+ ///
+ /// @param prefix prefix of the pool
+ /// @param prefix_len length of the pool prefix
+ /// @throw BadValue if the prefix family does not match
+ /// the set's family, prefix length is invalid, or the pool is
+ /// already in the set.
+ void insert(const isc::asiolink::IOAddress& prefix, uint8_t prefix_len);
+
+ /// @brief Checks if an address is present in the set.
+ ///
+ /// @param address address to look for.
+ /// @return true if the address is within a pool in the pool set
+ /// @throw BadValue if the address's family does not match
+ /// the set's family.
+ bool contains(const isc::asiolink::IOAddress& address) const;
+
+ /// @brief Returns the number of pools in the set.
+ size_t size() const {
+ return (pools_.size());
+ }
+
+ /// @brief Returns the protocol family of the address set.
+ uint16_t getFamily() const {
+ return (family_);
+ }
+
+private:
+ /// @brief protocol family of the set (AF_INET or AF_INET6)
+ uint16_t family_;
+
+ /// @brief Unique set of pools.
+ PoolRangeSet pools_;
+};
+
/// @brief Provides configuration and control flow for processing queries.
class LeaseQueryImpl : public boost::noncopyable {
public:
return (address_list_.size());
}
+ /// @brief Returns the number of valid requester pools.
+ size_t getNumRequesterPools() const {
+ return (pool_set_.size());
+ }
+
/// @brief Processes a single client Lease Query
///
/// - Validates query content
static size_t PageSize;
private:
+ /// @brief Parses 'requesters' list element.
+ ///
+ /// @param requesters pointer to the list element containing requestor
+ /// entris. Entries may be a mix of IP addresses or subnets in CIDR format.
+ ///
+ /// @throw BadValue if the list is empty or if any of the entries are
+ /// not valid addresses or CIDRs.
+ void parserRequesters(isc::data::ConstElementPtr requesters);
/// @brief The I/O context.
isc::asiolink::IOServicePtr io_service_;
/// @param list of addresses from which queries can be accepted.
AddressList address_list_;
+
+ /// @param set of valid requester pools.
+ PoolSet pool_set_;
};
/// @brief Defines a smart pointer to LeaseQueryImpl instance.
TEST_F(MemfileBulkLeaseQuery4ProcessTest, validConfig4) {
// Create an implementation with two requesters.
const string json = "{\n"
- " \"requesters\" : [ \"127.0.0.1\", \"192.0.2.2\" ],\n"
+ " \"requesters\" : [ \"127.0.0.1\", \"192.0.2.2\" , \"192.0.3.0/24\" ],\n"
" \"advanced\": {\n"
" \"bulk-query-enabled\": true,\n"
" \"active-query-enabled\": false,\n"
EXPECT_FALSE(impl->isRequester(IOAddress("192.0.2.1")));
EXPECT_TRUE(impl->isRequester(IOAddress("127.0.0.1")));
EXPECT_TRUE(impl->isRequester(IOAddress("192.0.2.2")));
+ EXPECT_TRUE(impl->isRequester(IOAddress("192.0.3.17")));
// Make sure a test with a v6 address complains.
ASSERT_THROW_MSG(impl->isRequester(IOAddress("2001:db8:1::1")), BadValue,
TEST_F(MemfileBulkLeaseQuery6ProcessTest, validConfig6) {
// Create an implementation with two requesters.
const string json = "{\n"
- " \"requesters\" : [ \"2001:db8:1::1\", \"2001:db8:1::3\" ],\n"
+ " \"requesters\" : [ \"2001:db8:1::1\", \"2001:db8:1::3\", \"3001::/64\" ],\n"
" \"prefix-lengths\": [ 72, 64 ],\n"
" \"advanced\": {\n"
" \"bulk-query-enabled\": true,\n"
EXPECT_TRUE(impl->isRequester(IOAddress("2001:db8:1::1")));
EXPECT_FALSE(impl->isRequester(IOAddress("2001:db8:1::2")));
EXPECT_TRUE(impl->isRequester(IOAddress("2001:db8:1::3")));
+ EXPECT_TRUE(impl->isRequester(IOAddress("3001::1")));
// Make sure a test with a v4 address complains.
ASSERT_THROW_MSG(impl->isRequester(IOAddress("192.0.2.1")), BadValue,
{
"requesters list is empty",
Element::fromJSON("{ \"requesters\" : [] }"),
- "'requesters' address list cannot be empty"
+ "'requesters' list cannot be empty"
},
{
"requesters entry not an address",
Element::fromJSON("{ \"requesters\" : [ \"foo\" ] }"),
- "'requesters' entry 'foo' is invalid: Failed to convert"
+ "'requesters' address entry 'foo' is invalid: Failed to convert"
" string to address 'foo': Invalid argument"
},
{
"requesters entry not a v4 address",
Element::fromJSON("{ \"requesters\" : [ \"2001:db8:1::\" ] }"),
- "'requesters' entry '2001:db8:1::' is invalid: not a IPv4 address"
+ "'requesters' address entry '2001:db8:1::' is invalid: not a IPv4 address"
},
{
"requesters entry is a duplicate",
Element::fromJSON("{ \"requesters\" : [ \"192.0.2.1\", \"192.0.2.1\" ] }"),
- "'requesters' entry '192.0.2.1' is invalid: address is already in the list"
+ "'requesters' address entry '192.0.2.1' is invalid: address is already in the list"
+ },
+ {
+ "requesters CIDR entry address is a invalid",
+ Element::fromJSON("{ \"requesters\" : [ \"192.0.2.x/24\" ] }"),
+ "'requesters' CIDR entry '192.0.2.x/24' is invalid:"
+ " Failed to convert string to address '192.0.2.x': Invalid argument"
+ },
+ {
+ "requesters CIDR length is a invalid",
+ Element::fromJSON("{ \"requesters\" : [ \"192.0.2.1/777\" ] }"),
+ "'requesters' CIDR entry '192.0.2.1/777' is invalid:"
+ " prefix length 777 is out of range"
+ },
+ {
+ "requesters CIDR entry is a duplicate",
+ Element::fromJSON("{ \"requesters\" : [ \"192.0.2.0/24\", \"192.0.2.0/24\" ] }"),
+ "'requesters' CIDR entry '192.0.2.0/24' is invalid: entry already exists"
}
};
"not a IPv4 address");
}
+// Verifies that valid v4 configuration using only CIDR entries
+// parses and that requesters can be validated.
+TEST(LeaseQueryImpl4Test, validConfig4CIDROnly) {
+ // Create an implementation with two requesters.
+ const std::string json = "{ \"requesters\" : [ \"192.0.2.0/24\", \"192.0.3.0/24\" ] }";
+ ConstElementPtr config;
+ ASSERT_NO_THROW_LOG(config = Element::fromJSON(json));
+
+ LeaseQueryImpl4Ptr impl;
+ ASSERT_NO_THROW_LOG(impl.reset(new LeaseQueryImpl4(config)));
+
+ // Verify known and unknown requesters check correctly.
+ EXPECT_TRUE(impl->isRequester(IOAddress("192.0.2.0")));
+ EXPECT_TRUE(impl->isRequester(IOAddress("192.0.2.10")));
+ EXPECT_TRUE(impl->isRequester(IOAddress("192.0.2.255")));
+ EXPECT_TRUE(impl->isRequester(IOAddress("192.0.3.80")));
+ EXPECT_TRUE(impl->isRequester(IOAddress("192.0.3.255")));
+ EXPECT_FALSE(impl->isRequester(IOAddress("192.0.4.255")));
+}
+
+// Verifies that valid v4 configuration both address and CIDR entries
+// parses and that requesters can be validated.
+TEST(LeaseQueryImpl4Test, validConfig4Mix) {
+ // Create an implementation with two requesters.
+ const std::string json = "{ \"requesters\" : [ \"192.0.2.0/24\", \"192.0.3.25\" ] }";
+ ConstElementPtr config;
+ ASSERT_NO_THROW_LOG(config = Element::fromJSON(json));
+
+ LeaseQueryImpl4Ptr impl;
+ ASSERT_NO_THROW_LOG(impl.reset(new LeaseQueryImpl4(config)));
+
+ // Verify known and unknown requesters check correctly.
+ EXPECT_TRUE(impl->isRequester(IOAddress("192.0.2.0")));
+ EXPECT_TRUE(impl->isRequester(IOAddress("192.0.2.10")));
+ EXPECT_TRUE(impl->isRequester(IOAddress("192.0.3.25")));
+ EXPECT_FALSE(impl->isRequester(IOAddress("192.0.3.255")));
+ EXPECT_FALSE(impl->isRequester(IOAddress("192.0.4.255")));
+}
+
// Verifies the invalid combinations of query parameters (ciaddr, HWAddr,
// and client id) are detected.
TEST(LeaseQueryImpl4Test, processQueryInvalidQuery) {
{
"requesters list is empty",
Element::fromJSON("{ \"requesters\" : [] }"),
- "'requesters' address list cannot be empty"
+ "'requesters' list cannot be empty"
},
{
"requesters entry not an address",
Element::fromJSON("{ \"requesters\" : [ \"foo\" ] }"),
- "'requesters' entry 'foo' is invalid: Failed to convert"
- " string to address 'foo': Invalid argument"
+ "'requesters' address entry 'foo' is invalid: Failed to convert string"
+ " to address 'foo': Invalid argument"
},
{
"requesters entry not a v6 address",
Element::fromJSON("{ \"requesters\" : [ \"192.0.2.1\" ] }"),
- "'requesters' entry '192.0.2.1' is invalid: not a IPv6 address"
+ "'requesters' address entry '192.0.2.1' is invalid: not a IPv6 address"
},
{
"requesters entry is a duplicate",
Element::fromJSON("{ \"requesters\" : [ \"2001:db8:1::\", \"2001:db8:1::\" ] }"),
- "'requesters' entry '2001:db8:1::' is invalid: address is already in the list"
+ "'requesters' address entry '2001:db8:1::' is invalid: address is already in the list"
+ },
+ {
+ "requesters CIDR entry address is invalid",
+ Element::fromJSON("{ \"requesters\" : [ \"2001:db8:1::x/64\" ] }"),
+ "'requesters' CIDR entry '2001:db8:1::x/64' is invalid:"
+ " Failed to convert string to address '2001:db8:1::x': Invalid argument"
+
+ },
+ {
+ "requesters CIDR entry length is invalid",
+ Element::fromJSON("{ \"requesters\" : [ \"2001:db8:1::/777\" ] }"),
+ "'requesters' CIDR entry '2001:db8:1::/777' is invalid:"
+ " prefix length 777 is out of range"
+ },
+ {
+ "requesters CIDR is a duplicate",
+ Element::fromJSON("{ \"requesters\" : [ \"2001:db8:1::/64\", \"2001:db8:1::/64\"] }"),
+ "'requesters' CIDR entry '2001:db8:1::/64' is invalid: entry already exists"
},
{
"prefix_lengths not a list",