From bd33d3bbdbe6f5a953efbe1f1876cc7f69a728bf Mon Sep 17 00:00:00 2001 From: Francis Dupont Date: Fri, 3 Jul 2020 01:47:41 +0200 Subject: [PATCH] [#1254] Updated tests and doc --- ChangeLog | 7 ++ doc/sphinx/arm/dhcp4-srv.rst | 5 + doc/sphinx/arm/dhcp6-srv.rst | 5 + src/bin/dhcp4/tests/host_unittest.cc | 15 +-- .../dhcpsrv/tests/cfg_subnets4_unittest.cc | 57 ++++++++++ .../dhcpsrv/tests/cfg_subnets6_unittest.cc | 104 ++++++++++++++++++ 6 files changed, 186 insertions(+), 7 deletions(-) diff --git a/ChangeLog b/ChangeLog index 05a4a4adf6..0df15559ff 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,10 @@ +17xx. [bug]* fdupont + When a host reservation in a subnet reserves an address, the + address must in the subnet prefix. This check was previously + only done by the host command hook library. Note it does not + applies the prefix delegation. + (Gitlab #1254) + 1787. [bug] razvan The recount leases functions consider leases in 'declined' state as 'assigned' so that when the lease is reclaimed or reused, no negative diff --git a/doc/sphinx/arm/dhcp4-srv.rst b/doc/sphinx/arm/dhcp4-srv.rst index 2802df14ec..65eab79b77 100644 --- a/doc/sphinx/arm/dhcp4-srv.rst +++ b/doc/sphinx/arm/dhcp4-srv.rst @@ -3841,6 +3841,11 @@ In most cases an IP address will be specified. It is also possible to specify a hostname, host-specific options, or fields carried within the DHCPv4 message such as siaddr, sname, or file. +.. note:: + + Beginning with Kea 1.7.11 the reserved address must be in the subnet + prefix. + The following example shows how to reserve addresses for specific hosts in a subnet: diff --git a/doc/sphinx/arm/dhcp6-srv.rst b/doc/sphinx/arm/dhcp6-srv.rst index 2c16ec8fcf..86a5b1fe85 100644 --- a/doc/sphinx/arm/dhcp6-srv.rst +++ b/doc/sphinx/arm/dhcp6-srv.rst @@ -3354,6 +3354,11 @@ usually a DUID, but it can also be a hardware or MAC address. One or more addresses or prefixes may also be specified, and it is possible to specify a hostname and DHCPv6 options for a given host. +.. note:: + + Beginning with Kea 1.7.11 all reserved addresses must be in the subnet + prefix. This does not apply to reserved prefixes. + The following example shows how to reserve addresses and prefixes for specific hosts: diff --git a/src/bin/dhcp4/tests/host_unittest.cc b/src/bin/dhcp4/tests/host_unittest.cc index 6ef2573a83..de109c606c 100644 --- a/src/bin/dhcp4/tests/host_unittest.cc +++ b/src/bin/dhcp4/tests/host_unittest.cc @@ -134,7 +134,7 @@ const char* CONFIGS[] = { " {\n" " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n" " \"hostname\": \"subnet-10-host\",\n" - " \"ip-address\": \"192.0.5.10\"\n" + " \"ip-address\": \"10.0.0.105\"\n" " }]\n" " }\n" "]\n" @@ -154,14 +154,15 @@ const char* CONFIGS[] = { "\"subnet4\": [\n" " {\n" " \"subnet\": \"10.0.0.0/24\", \n" - " \"id\": 10," " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],\n" + " \"id\": 10," + " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],\n" " \"interface\": \"eth0\",\n" " \"reservation-mode\": \"all\"," " \"reservations\": [ \n" " {\n" " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n" " \"hostname\": \"subnet-10-host\",\n" - " \"ip-address\": \"192.0.5.10\"\n" + " \"ip-address\": \"10.0.0.105\"\n" " }]\n" " }\n" "]\n" @@ -550,9 +551,9 @@ TEST_F(HostTest, outOfPoolOverGlobal) { // Hardware address matches all reservations client.setHWAddress("aa:bb:cc:dd:ee:ff"); - // Subnet 10 usses default HR mode(i.e. "in-pool"), so its + // Subnet 10 uses "out-of-pool" HR mode, so its // reservation should be used, rather than global. - runDoraTest(CONFIGS[2], client, "subnet-10-host", "192.0.5.10"); + runDoraTest(CONFIGS[2], client, "subnet-10-host", "10.0.0.105"); } // Verifies that when there are matching reservations at @@ -565,9 +566,9 @@ TEST_F(HostTest, allOverGlobal) { // Hardware address matches all reservations client.setHWAddress("aa:bb:cc:dd:ee:ff"); - // Subnet 10 usses default HR mode(i.e. "in-pool"), so its + // Subnet 10 uses default HR mode(i.e. "all"), so its // reservation should be used, rather than global. - runDoraTest(CONFIGS[3], client, "subnet-10-host", "192.0.5.10"); + runDoraTest(CONFIGS[3], client, "subnet-10-host", "10.0.0.105"); } // Verifies that client class specified in the global reservation diff --git a/src/lib/dhcpsrv/tests/cfg_subnets4_unittest.cc b/src/lib/dhcpsrv/tests/cfg_subnets4_unittest.cc index 2e03260d59..75b0ba2c28 100644 --- a/src/lib/dhcpsrv/tests/cfg_subnets4_unittest.cc +++ b/src/lib/dhcpsrv/tests/cfg_subnets4_unittest.cc @@ -22,6 +22,8 @@ #include #include #include +#include +#include #include #include #include @@ -1900,5 +1902,60 @@ TEST(CfgSubnets4Test, removeStatistics) { "reclaimed-leases")); ASSERT_FALSE(observation); } + +// This test verifies that in range host reservation works as expected. +TEST(CfgSubnets4Test, host) { + // Create a configuration. + std::string json = + " {" + " \"id\": 1,\n" + " \"subnet\": \"10.1.2.0/24\", \n" + " \"reservations\": [ {\n" + " \"hw-address\": \"aa:bb:cc:dd:ee:ff\", \n" + " \"ip-address\": \"10.1.2.1\" } ]\n" + " }"; + + data::ElementPtr elems; + ASSERT_NO_THROW(elems = data::Element::fromJSON(json)) + << "invalid JSON:" << json << "\n test is broken"; + + Subnet4ConfigParser parser; + Subnet4Ptr subnet; + EXPECT_NO_THROW(subnet = parser.parse(elems)); + ASSERT_TRUE(subnet); + CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts(); + ASSERT_TRUE(cfg_hosts); + HostCollection hosts = cfg_hosts->getAll4(SubnetID(1)); + ASSERT_EQ(1, hosts.size()); + ConstHostPtr host = hosts[0]; + ASSERT_TRUE(host); + EXPECT_EQ(1, host->getIPv4SubnetID()); + EXPECT_EQ("hwaddr=AABBCCDDEEFF", host->getIdentifierAsText()); + EXPECT_EQ("10.1.2.1", host->getIPv4Reservation().toText()); + + CfgMgr::instance().clear(); +} + +// This test verifies that an out of range host reservation is rejected. +TEST(CfgSubnets4Test, outOfRangeHost) { + // Create a configuration. + std::string json = + " {" + " \"id\": 1,\n" + " \"subnet\": \"10.1.2.0/24\", \n" + " \"reservations\": [ {\n" + " \"hw-address\": \"aa:bb:cc:dd:ee:ff\", \n" + " \"ip-address\": \"10.2.2.1\" } ]\n" + " }"; + + data::ElementPtr elems; + ASSERT_NO_THROW(elems = data::Element::fromJSON(json)) + << "invalid JSON:" << json << "\n test is broken"; + + Subnet4ConfigParser parser; + std::string msg = "specified reservation '10.2.2.1' is not matching "; + msg += "the IPv4 subnet prefix '10.1.2.0/24'"; + EXPECT_THROW_MSG(parser.parse(elems), DhcpConfigError, msg); +} } // end of anonymous namespace diff --git a/src/lib/dhcpsrv/tests/cfg_subnets6_unittest.cc b/src/lib/dhcpsrv/tests/cfg_subnets6_unittest.cc index 35fb0c55b1..d32d8555d8 100644 --- a/src/lib/dhcpsrv/tests/cfg_subnets6_unittest.cc +++ b/src/lib/dhcpsrv/tests/cfg_subnets6_unittest.cc @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include #include #include @@ -1774,4 +1776,106 @@ TEST(CfgSubnets6Test, removeStatistics) { ASSERT_FALSE(observation); } +// This test verifies that in range host reservation works as expected. +TEST(CfgSubnets6Test, hostNA) { + // Create a configuration. + std::string json = + " {" + " \"id\": 1,\n" + " \"subnet\": \"2001:db8:1::/48\", \n" + " \"reservations\": [ {\n" + " \"hw-address\": \"aa:bb:cc:dd:ee:ff\", \n" + " \"ip-addresses\": [\"2001:db8:1::1\"] } ]\n" + " }"; + + data::ElementPtr elems; + ASSERT_NO_THROW(elems = data::Element::fromJSON(json)) + << "invalid JSON:" << json << "\n test is broken"; + + Subnet6ConfigParser parser; + Subnet6Ptr subnet; + EXPECT_NO_THROW(subnet = parser.parse(elems)); + ASSERT_TRUE(subnet); + CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts(); + ASSERT_TRUE(cfg_hosts); + HostCollection hosts = cfg_hosts->getAll6(SubnetID(1)); + ASSERT_EQ(1, hosts.size()); + ConstHostPtr host = hosts[0]; + ASSERT_TRUE(host); + EXPECT_EQ(1, host->getIPv6SubnetID()); + EXPECT_EQ("hwaddr=AABBCCDDEEFF", host->getIdentifierAsText()); + IPv6ResrvRange addresses = host->getIPv6Reservations(IPv6Resrv::TYPE_NA); + ASSERT_EQ(1, std::distance(addresses.first, addresses.second)); + EXPECT_EQ("2001:db8:1::1", addresses.first->second.getPrefix().toText()); + + CfgMgr::instance().clear(); +} + +// This test verifies that an out of range host reservation is rejected. +TEST(CfgSubnets6Test, outOfRangeHost) { + // Create a configuration. + std::string json = + " {" + " \"id\": 1,\n" + " \"subnet\": \"2001:db8:1::/48\", \n" + " \"reservations\": [ {\n" + " \"hw-address\": \"aa:bb:cc:dd:ee:ff\", \n" + " \"ip-addresses\": [\"2001:db8:1::1\", \n" + " \"2001:db8:2::1\"] } ]\n" + " }"; + + data::ElementPtr elems; + ASSERT_NO_THROW(elems = data::Element::fromJSON(json)) + << "invalid JSON:" << json << "\n test is broken"; + + Subnet6ConfigParser parser; + std::string msg = "specified reservation '2001:db8:2::1' is "; + msg += "not matching the IPv6 subnet prefix '2001:db8:1::/48'"; + EXPECT_THROW_MSG(parser.parse(elems), DhcpConfigError, msg); +} + +// This test verifies that in range validation does not applies to prefixes. +TEST(CfgSubnets6Test, hostPD) { + // Create a configuration. + std::string json = + " {" + " \"id\": 1,\n" + " \"subnet\": \"2001:db8:1::/48\", \n" + " \"reservations\": [ {\n" + " \"hw-address\": \"aa:bb:cc:dd:ee:ff\", \n" + " \"prefixes\": [\"2001:db8:2::/64\"] } ]\n" + " }"; + + data::ElementPtr elems; + ASSERT_NO_THROW(elems = data::Element::fromJSON(json)) + << "invalid JSON:" << json << "\n test is broken"; + + Subnet6ConfigParser parser; + Subnet6Ptr subnet; + try { + subnet = parser.parse(elems); + } catch (const std::exception& ex) { + std::cerr << "parse fail with " << ex.what() << std::endl; + } + //EXPECT_NO_THROW(subnet = parser.parse(elems)); + ASSERT_TRUE(subnet); + CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts(); + ASSERT_TRUE(cfg_hosts); + HostCollection hosts = cfg_hosts->getAll6(SubnetID(1)); + ASSERT_EQ(1, hosts.size()); + ConstHostPtr host = hosts[0]; + ASSERT_TRUE(host); + EXPECT_EQ(1, host->getIPv6SubnetID()); + EXPECT_EQ("hwaddr=AABBCCDDEEFF", host->getIdentifierAsText()); + IPv6ResrvRange prefixes = host->getIPv6Reservations(IPv6Resrv::TYPE_PD); + ASSERT_EQ(1, std::distance(prefixes.first, prefixes.second)); + EXPECT_EQ("2001:db8:2::", prefixes.first->second.getPrefix().toText()); + EXPECT_EQ(64, prefixes.first->second.getPrefixLen()); + + // Verify the prefix is not in the subnet range. + EXPECT_FALSE(subnet->inRange(prefixes.first->second.getPrefix())); + + CfgMgr::instance().clear(); +} + } // end of anonymous namespace -- 2.47.2