From: Tomek Mrugalski Date: Mon, 29 Oct 2012 12:18:27 +0000 (+0100) Subject: [master] Merge branch 'trac2324' (DHCPv6 allocation engine) X-Git-Tag: trac2487_base~21^2~9 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=869e658fbc3e33c5037f3866beb2ff76c03cbdf4;p=thirdparty%2Fkea.git [master] Merge branch 'trac2324' (DHCPv6 allocation engine) Conflicts: ChangeLog src/lib/dhcp/subnet.cc src/lib/dhcp/subnet.h src/lib/dhcp/tests/subnet_unittest.cc --- 869e658fbc3e33c5037f3866beb2ff76c03cbdf4 diff --cc ChangeLog index e31f44ca55,6ec7aec8bb..b7ed58b4a6 --- a/ChangeLog +++ b/ChangeLog @@@ -1,10 -1,9 +1,17 @@@ -4XX. [func] tomek ++496. [func] tomek + DHCPv6 Allocation Engine implemented. It allows address allocation + from the configured subnets/pools. It currently features a single + allocator: IterativeAllocator, which assigns addresses iteratively. + Other allocators (hashed, random) are planned. - (Trac #2324, git TBD) ++ (Trac #2324, git 8aa188a10298e3a55b725db36502a99d2a8d638a) ++ +495. [func] team + b10-auth now handles reconfiguration of data sources in + background using a separate thread. This means even if the new + configuration includes a large amount of data to be loaded into + memory (very large zones and/or a very large number of zones), + the reconfiguration doesn't block query handling. + (Multiple Trac tickets up to #2211) 494. [bug] jinmei Fixed a problem that shutting down BIND 10 kept some of the diff --cc src/lib/dhcp/subnet.cc index cb82a9f12f,230a1b9ebd..f6ce1b154a --- a/src/lib/dhcp/subnet.cc +++ b/src/lib/dhcp/subnet.cc @@@ -96,15 -85,23 +96,31 @@@ Pool4Ptr Subnet4::getPool4(const isc::a return (candidate); } +void +Subnet4::validateOption(const OptionPtr& option) const { + if (!option) { + isc_throw(isc::BadValue, "option configured for subnet must not be NULL"); + } else if (option->getUniverse() != Option::V4) { + isc_throw(isc::BadValue, "expected V4 option to be added to the subnet"); + } +} + + bool Subnet4::inPool(const isc::asiolink::IOAddress& addr) const { + + // Let's start with checking if it even belongs to that subnet. + if (!inRange(addr)) { + return (false); + } + + for (Pool4Collection::const_iterator pool = pools_.begin(); pool != pools_.end(); ++pool) { + if ((*pool)->inRange(addr)) { + return (true); + } + } + // there's no pool that address belongs to + return (false); + } + - Subnet6::Subnet6(const isc::asiolink::IOAddress& prefix, uint8_t length, const Triplet& t1, const Triplet& t2, @@@ -151,13 -148,21 +167,30 @@@ Pool6Ptr Subnet6::getPool6(const isc::a return (candidate); } +void +Subnet6::validateOption(const OptionPtr& option) const { + if (!option) { + isc_throw(isc::BadValue, "option configured for subnet must not be NULL"); + } else if (option->getUniverse() != Option::V6) { + isc_throw(isc::BadValue, "expected V6 option to be added to the subnet"); + } +} ++ + bool Subnet6::inPool(const isc::asiolink::IOAddress& addr) const { + + // Let's start with checking if it even belongs to that subnet. + if (!inRange(addr)) { + return (false); + } + + for (Pool6Collection::const_iterator pool = pools_.begin(); pool != pools_.end(); ++pool) { + if ((*pool)->inRange(addr)) { + return (true); + } + } + // there's no pool that address belongs to + return (false); + } + } // end of isc::dhcp namespace } // end of isc namespace diff --cc src/lib/dhcp/subnet.h index aa680ce593,714ed9e502..894d807990 --- a/src/lib/dhcp/subnet.h +++ b/src/lib/dhcp/subnet.h @@@ -36,174 -30,33 +36,195 @@@ namespace dhcp /// attached to it. In most cases all devices attached to a single link can /// share the same parameters. Therefore Subnet holds several values that are /// typically shared by all hosts: renew timer (T1), rebind timer (T2) and -/// leased addresses lifetime (valid-lifetime). +/// leased addresses lifetime (valid-lifetime). It also holds the set +/// of DHCP option instances configured for the subnet. These options are +/// included in DHCP messages being sent to clients which are connected +/// to the particular subnet. + /// + /// @todo: Implement support for options here + + + /// @brief Unique indentifier for a subnet (both v4 and v6) + typedef uint32_t SubnetID; + class Subnet { public: + + /// @brief Option descriptor. + /// + /// Option descriptor holds information about option configured for + /// a particular subnet. This information comprises the actual option + /// instance and information whether this option is sent to DHCP client + /// only on request (persistent = false) or always (persistent = true). + struct OptionDescriptor { + /// Option instance. + OptionPtr option; + /// Persistent flag, if true option is always sent to the client, + /// if false option is sent to the client on request. + bool persistent; + + /// @brief Constructor. + /// + /// @param opt option + /// @param persist if true option is always sent. + OptionDescriptor(OptionPtr& opt, bool persist) + : option(opt), persistent(persist) {}; + }; + + /// @brief Extractor class to extract key with another key. + /// + /// This class solves the problem of accessing index key values + /// that are stored in objects nested in other objects. + /// Each OptionDescriptor structure contains the OptionPtr object. + /// The value retured by one of its accessors (getType) is used + /// as an indexing value in the multi_index_container defined below. + /// There is no easy way to mark that value returned by Option::getType + /// should be an index of this multi_index_container. There are standard + /// key extractors such as 'member' or 'mem_fun' but they are not + /// sufficient here. The former can be used to mark that member of + /// the structure that is held in the container should be used as an + /// indexing value. The latter can be used if the indexing value is + /// a product of the class being held in the container. In this complex + /// scenario when the indexing value is a product of the function that + /// is wrapped by the structure, this new extractor template has to be + /// defined. The template class provides a 'chain' of two extractors + /// to access the value returned by nested object and to use it as + /// indexing value. + /// For some more examples of complex keys see: + /// http://www.cs.brown.edu/~jwicks/boost/libs/multi_index/doc/index.html + /// + /// @tparam KeyExtractor1 extractor used to access data in + /// OptionDescriptor::option + /// @tparam KeyExtractor2 extractor used to access + /// OptionDescriptor::option member. + template + class KeyFromKey { + public: + typedef typename KeyExtractor1::result_type result_type; + + /// @brief Constructor. + KeyFromKey() + : key1_(KeyExtractor1()), key2_(KeyExtractor2()) { }; + + /// @brief Extract key with another key. + /// + /// @param arg the key value. + /// + /// @tparam key value type. + template + result_type operator() (T& arg) const { + return (key1_(key2_(arg))); + } + private: + KeyExtractor1 key1_; ///< key 1. + KeyExtractor2 key2_; ///< key 2. + }; + + /// @brief Multi index container for DHCP option descriptors. + /// + /// This container comprises three indexes to access option + /// descriptors: + /// - sequenced index: used to access elements in the order they + /// have been added to the container, + /// - option type index: used to search option descriptors containing + /// options with specific option code (aka option type). + /// - persistency flag index: used to search option descriptors with + /// 'persistent' flag set to true. + /// + /// This container is the equivalent of three separate STL containers: + /// - std::list of all options, + /// - std::multimap of options with option code used as a multimap key, + /// - std::multimap of option descriptors with option persistency flag + /// used as a multimap key. + /// The major advantage of this container over 3 separate STL containers + /// is automatic synchronization of all indexes when elements are added, + /// removed or modified in the container. With separate containers, + /// the synchronization would have to be guaranteed by the Subnet class + /// code. This would increase code complexity and presumably it would + /// be much harder to add new search criteria (indexes). + /// + /// @todo we may want to search for options using option spaces when + /// they are implemented. + /// + /// @see http://www.boost.org/doc/libs/1_51_0/libs/multi_index/doc/index.html + typedef boost::multi_index_container< + // Container comprises elements of OptionDescriptor type. + OptionDescriptor, + // Here we start enumerating various indexes. + boost::multi_index::indexed_by< + // Sequenced index allows accessing elements in the same way + // as elements in std::list. + // Sequenced is an index #0. + boost::multi_index::sequenced<>, + // Start definition of index #1. + boost::multi_index::hashed_non_unique< + // KeyFromKey is the index key extractor that allows accessing + // option type being held by the OptionPtr through + // OptionDescriptor structure. + KeyFromKey< + // Use option type as the index key. The type is held + // in OptionPtr object so we have to call Option::getType + // to retrieve this key for each element. + boost::multi_index::mem_fun< + Option, + uint16_t, + &Option::getType + >, + // Indicate that OptionPtr is a member of + // OptionDescriptor structure. + boost::multi_index::member< + OptionDescriptor, + OptionPtr, + &OptionDescriptor::option + > + > + >, + // Start definition of index #2. + // Use 'persistent' struct member as a key. + boost::multi_index::hashed_non_unique< + boost::multi_index::member< + OptionDescriptor, + bool, + &OptionDescriptor::persistent + > + > + > + > OptionContainer; + + /// Type of the index #1 - option type. + typedef OptionContainer::nth_index<1>::type OptionContainerTypeIndex; + /// Type of the index #2 - option persistency flag. + typedef OptionContainer::nth_index<2>::type OptionContainerPersistIndex; + /// @brief checks if specified address is in range bool inRange(const isc::asiolink::IOAddress& addr) const; + /// @brief Add new option instance to the collection. + /// + /// @param option option instance. + /// @param persistent if true, send an option regardless if client + /// requested it or not. + /// + /// @throw isc::BadValue if invalid option provided. + void addOption(OptionPtr& option, bool persistent = false); + + /// @brief Delete all options configured for the subnet. + void delOptions(); + + /// @brief checks if the specified address is in pools + /// + /// Note the difference between inSubnet() and inPool(). For a given + /// subnet (e.g. 2001::/64) there may be one or more pools defined + /// that may or may not cover entire subnet, e.g. pool 2001::1-2001::10). + /// inPool() returning true implies inSubnet(), but the reverse implication + /// is not always true. For the given example, 2001::1234:abcd would return + /// true for inSubnet(), but false for inPool() check. + /// + /// @param addr this address will be checked if it belongs to any pools in + /// that subnet + /// @return true if the address is in any of the pools + virtual bool inPool(const isc::asiolink::IOAddress& addr) const = 0; + /// @brief return valid-lifetime for addresses in that prefix Triplet getValid() const { return (valid_); @@@ -219,15 -72,36 +240,45 @@@ return (t2_); } + /// @brief Return a collection of options. + /// + /// @return reference to collection of options configured for a subnet. + /// The returned reference is valid as long as the Subnet object which + /// returned it still exists. + const OptionContainer& getOptions() { + return (options_); + } + + /// @brief returns the last address that was tried from this pool + /// + /// This method returns the last address that was attempted to be allocated + /// from this subnet. This is used as helper information for the next + /// iteration of the allocation algorithm. + /// + /// @todo: Define map somewhere in the + /// AllocEngine::IterativeAllocator and keep the data there + /// + /// @return address that was last tried from this pool + isc::asiolink::IOAddress getLastAllocated() const { + return (last_allocated_); + } + + /// @brief sets the last address that was tried from this pool + /// + /// This method sets the last address that was attempted to be allocated + /// from this subnet. This is used as helper information for the next + /// iteration of the allocation algorithm. + /// + /// @todo: Define map somewhere in the + /// AllocEngine::IterativeAllocator and keep the data there + void setLastAllocated(const isc::asiolink::IOAddress& addr) { + last_allocated_ = addr; + } + + /// @brief returns unique ID for that subnet + /// @return unique ID for that subnet + SubnetID getID() const { return (id_); } + protected: /// @brief protected constructor // @@@ -278,8 -145,16 +329,19 @@@ /// @brief a tripet (min/default/max) holding allowed valid lifetime values Triplet valid_; + /// @brief a collection of DHCP options configured for a subnet. + OptionContainer options_; ++ + /// @brief last allocated address + /// + /// This is the last allocated address that was previously allocated from + /// this particular subnet. Some allocation algorithms (e.g. iterative) use + /// that value, others do not. It should be noted that although the value + /// is usually correct, there are cases when it is invalid, e.g. after + /// removing a pool, restarting or changing allocation algorithms. For + /// that purpose it should be only considered a help that should not be + /// fully trusted. + isc::asiolink::IOAddress last_allocated_; }; /// @brief A configuration holder for IPv4 subnet. @@@ -321,15 -195,15 +382,23 @@@ public return pools_; } + /// @brief checks if the specified address is in pools + /// + /// See the description in \ref Subnet::inPool(). + /// + /// @param addr this address will be checked if it belongs to any pools in that subnet + /// @return true if the address is in any of the pools + bool inPool(const isc::asiolink::IOAddress& addr) const; + protected: + + /// @brief Check if option is valid and can be added to a subnet. + /// + /// @param option option to be validated. + /// + /// @throw isc::BadValue if provided option is invalid. + virtual void validateOption(const OptionPtr& option) const; + /// @brief collection of pools in that list Pool4Collection pools_; }; @@@ -389,15 -263,15 +458,23 @@@ public return pools_; } + /// @brief checks if the specified address is in pools + /// + /// See the description in \ref Subnet::inPool(). + /// + /// @param addr this address will be checked if it belongs to any pools in that subnet + /// @return true if the address is in any of the pools + bool inPool(const isc::asiolink::IOAddress& addr) const; + protected: + + /// @brief Check if option is valid and can be added to a subnet. + /// + /// @param option option to be validated. + /// + /// @throw isc::BadValue if provided option is invalid. + virtual void validateOption(const OptionPtr& option) const; + /// @brief collection of pools in that list Pool6Collection pools_; diff --cc src/lib/dhcp/tests/Makefile.am index a816472327,cb9941c11f..fcbdec8136 --- a/src/lib/dhcp/tests/Makefile.am +++ b/src/lib/dhcp/tests/Makefile.am @@@ -56,8 -55,8 +58,9 @@@ libdhcpsrv_unittests_CXXFLAGS = $(AM_CX libdhcpsrv_unittests_LDADD = $(GTEST_LDADD) libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libb10-asiolink.la + libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libb10-dhcp++.la libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libb10-dhcpsrv.la +libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libb10-dhcp++.la libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/log/libb10-log.la diff --cc src/lib/dhcp/tests/subnet_unittest.cc index 0e2e846141,825c354386..be25bc1ac9 --- a/src/lib/dhcp/tests/subnet_unittest.cc +++ b/src/lib/dhcp/tests/subnet_unittest.cc @@@ -105,24 -104,41 +105,59 @@@ TEST(Subnet4Test, Subnet4_Pool4_checks EXPECT_THROW(subnet->addPool4(pool3), BadValue); } +TEST(Subnet4Test, addInvalidOption) { + // Create the V4 subnet. + Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.2.0"), 8, 1, 2, 3)); + + // Some dummy option code. + uint16_t code = 100; + // Create option with invalid universe (V6 instead of V4). + // Attempt to add this option should result in exception. + OptionPtr option1(new Option(Option::V6, code, OptionBuffer(10, 0xFF))); + EXPECT_THROW(subnet->addOption(option1), isc::BadValue); + + // Create NULL pointer option. Attempt to add NULL option + // should result in exception. + OptionPtr option2; + ASSERT_FALSE(option2); + EXPECT_THROW(subnet->addOption(option2), isc::BadValue); +} + + // This test verifies that inRange() and inPool() methods work properly. + TEST(Subnet4Test, inRangeinPool) { + Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.0.0"), 8, 1, 2, 3)); + + // this one is in subnet + Pool4Ptr pool1(new Pool4(IOAddress("192.2.0.0"), 16)); + subnet->addPool4(pool1); + + // 192.1.1.1 belongs to the subnet... + EXPECT_TRUE(subnet->inRange(IOAddress("192.1.1.1"))); + + // ... but it does not belong to any pool within + EXPECT_FALSE(subnet->inPool(IOAddress("192.1.1.1"))); + + // the last address that is in range, but out of pool + EXPECT_TRUE(subnet->inRange(IOAddress("192.1.255.255"))); + EXPECT_FALSE(subnet->inPool(IOAddress("192.1.255.255"))); + + // the first address that is in range, in pool + EXPECT_TRUE(subnet->inRange(IOAddress("192.2.0.0"))); + EXPECT_TRUE (subnet->inPool(IOAddress("192.2.0.0"))); + + // let's try something in the middle as well + EXPECT_TRUE(subnet->inRange(IOAddress("192.2.3.4"))); + EXPECT_TRUE (subnet->inPool(IOAddress("192.2.3.4"))); + + // the last address that is in range, in pool + EXPECT_TRUE(subnet->inRange(IOAddress("192.2.255.255"))); + EXPECT_TRUE (subnet->inPool(IOAddress("192.2.255.255"))); + + // the first address that is in range, but out of pool + EXPECT_TRUE(subnet->inRange(IOAddress("192.3.0.0"))); + EXPECT_FALSE(subnet->inPool(IOAddress("192.3.0.0"))); + } + // Tests for Subnet6 TEST(Subnet6Test, constructor) { @@@ -206,146 -221,40 +240,182 @@@ TEST(Subnet6Test, Subnet6_Pool6_checks EXPECT_THROW(subnet->addPool6(pool4), BadValue); } +TEST(Subnet6Test, addOptions) { + // Create as subnet to add options to it. + Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4)); + + // Differentiate options by their codes (100-109) + for (uint16_t code = 100; code < 110; ++code) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF))); + ASSERT_NO_THROW(subnet->addOption(option)); + } + + // Get options from the Subnet and check if all 10 are there. + Subnet::OptionContainer options = subnet->getOptions(); + ASSERT_EQ(10, options.size()); + + // Validate codes of added options. + uint16_t expected_code = 100; + for (Subnet::OptionContainer::const_iterator option_desc = options.begin(); + option_desc != options.end(); ++option_desc) { + ASSERT_TRUE(option_desc->option); + EXPECT_EQ(expected_code, option_desc->option->getType()); + ++expected_code; + } + + subnet->delOptions(); + + options = subnet->getOptions(); + EXPECT_EQ(0, options.size()); +} + +TEST(Subnet6Test, addNonUniqueOptions) { + // Create as subnet to add options to it. + Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4)); + + // Create a set of options with non-unique codes. + for (int i = 0; i < 2; ++i) { + // In the inner loop we create options with unique codes (100-109). + for (uint16_t code = 100; code < 110; ++code) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF))); + ASSERT_NO_THROW(subnet->addOption(option)); + } + } + + // Sanity check that all options are there. + Subnet::OptionContainer options = subnet->getOptions(); + ASSERT_EQ(20, options.size()); + + // Use container index #1 to get the options by their codes. + Subnet::OptionContainerTypeIndex& idx = options.get<1>(); + // Look for the codes 100-109. + for (uint16_t code = 100; code < 110; ++ code) { + // For each code we should get two instances of options. + std::pair range = + idx.equal_range(code); + // Distance between iterators indicates how many options + // have been retured for the particular code. + ASSERT_EQ(2, distance(range.first, range.second)); + // Check that returned options actually have the expected option code. + for (Subnet::OptionContainerTypeIndex::const_iterator option_desc = range.first; + option_desc != range.second; ++option_desc) { + ASSERT_TRUE(option_desc->option); + EXPECT_EQ(code, option_desc->option->getType()); + } + } + + // Let's try to find some non-exiting option. + const uint16_t non_existing_code = 150; + std::pair range = + idx.equal_range(non_existing_code); + // Empty set is expected. + EXPECT_EQ(0, distance(range.first, range.second)); + + subnet->delOptions(); + + options = subnet->getOptions(); + EXPECT_EQ(0, options.size()); +} + +TEST(Subnet6Test, addInvalidOption) { + // Create as subnet to add options to it. + Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4)); + + // Some dummy option code. + uint16_t code = 100; + // Create option with invalid universe (V4 instead of V6). + // Attempt to add this option should result in exception. + OptionPtr option1(new Option(Option::V4, code, OptionBuffer(10, 0xFF))); + EXPECT_THROW(subnet->addOption(option1), isc::BadValue); + + // Create NULL pointer option. Attempt to add NULL option + // should result in exception. + OptionPtr option2; + ASSERT_FALSE(option2); + EXPECT_THROW(subnet->addOption(option2), isc::BadValue); +} + +TEST(Subnet6Test, addPersistentOption) { + // Create as subnet to add options to it. + Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4)); + + // Add 10 options to the subnet with option codes 100 - 109. + for (uint16_t code = 100; code < 110; ++code) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF))); + // We create 10 options and want some of them to be flagged + // persistent and some non-persistent. Persistent options are + // those that server sends to clients regardless if they ask + // for them or not. We pick 3 out of 10 options and mark them + // non-persistent and 7 other options persistent. + // Code values: 102, 105 and 108 are divisable by 3 + // and options with these codes will be flagged non-persistent. + // Options with other codes will be flagged persistent. + bool persistent = (code % 3) ? true : false; + ASSERT_NO_THROW(subnet->addOption(option, persistent)); + } + + // Get added options from the subnet. + Subnet::OptionContainer options = subnet->getOptions(); + + // options.get<2> returns reference to container index #2. This + // index is used to access options by the 'persistent' flag. + Subnet::OptionContainerPersistIndex& idx = options.get<2>(); + + // Get all persistent options. + std::pair range_persistent = + idx.equal_range(true); + // 3 out of 10 options have been flagged persistent. + ASSERT_EQ(7, distance(range_persistent.first, range_persistent.second)); + + // Get all non-persistent options. + std::pair range_non_persistent = + idx.equal_range(false); + // 7 out of 10 options have been flagged persistent. + ASSERT_EQ(3, distance(range_non_persistent.first, range_non_persistent.second)); + + subnet->delOptions(); + + options = subnet->getOptions(); + EXPECT_EQ(0, options.size()); +} ++ + // This test verifies that inRange() and inPool() methods work properly. + TEST(Subnet6Test, inRangeinPool) { + Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8::"), 32, 1, 2, 3, 4)); + + // this one is in subnet + Pool6Ptr pool1(new Pool6(Pool6::TYPE_IA, IOAddress("2001:db8::10"), + IOAddress("2001:db8::20"))); + subnet->addPool6(pool1); + + // 192.1.1.1 belongs to the subnet... + EXPECT_TRUE(subnet->inRange(IOAddress("2001:db8::1"))); + // ... but it does not belong to any pool within + EXPECT_FALSE(subnet->inPool(IOAddress("2001:db8::1"))); + + // the last address that is in range, but out of pool + EXPECT_TRUE(subnet->inRange(IOAddress("2001:db8::f"))); + EXPECT_FALSE(subnet->inPool(IOAddress("2001:db8::f"))); + + // the first address that is in range, in pool + EXPECT_TRUE(subnet->inRange(IOAddress("2001:db8::10"))); + EXPECT_TRUE (subnet->inPool(IOAddress("2001:db8::10"))); + + // let's try something in the middle as well + EXPECT_TRUE(subnet->inRange(IOAddress("2001:db8::18"))); + EXPECT_TRUE (subnet->inPool(IOAddress("2001:db8::18"))); + + // the last address that is in range, in pool + EXPECT_TRUE(subnet->inRange(IOAddress("2001:db8::20"))); + EXPECT_TRUE (subnet->inPool(IOAddress("2001:db8::20"))); + + // the first address that is in range, but out of pool + EXPECT_TRUE(subnet->inRange(IOAddress("2001:db8::21"))); + EXPECT_FALSE(subnet->inPool(IOAddress("2001:db8::21"))); + } + - };