diff options
Diffstat (limited to 'src/bin/dhcp4/tests/host_unittest.cc')
-rw-r--r-- | src/bin/dhcp4/tests/host_unittest.cc | 843 |
1 files changed, 843 insertions, 0 deletions
diff --git a/src/bin/dhcp4/tests/host_unittest.cc b/src/bin/dhcp4/tests/host_unittest.cc new file mode 100644 index 0000000..5a39c85 --- /dev/null +++ b/src/bin/dhcp4/tests/host_unittest.cc @@ -0,0 +1,843 @@ +// Copyright (C) 2018-2023 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 <asiolink/io_address.h> +#include <cc/data.h> +#include <dhcp/dhcp4.h> +#include <dhcp/tests/iface_mgr_test_config.h> +#include <dhcpsrv/cfgmgr.h> +#include <dhcpsrv/host.h> +#include <dhcpsrv/host_mgr.h> +#include <dhcpsrv/subnet_id.h> +#include <dhcp4/tests/dhcp4_test_utils.h> +#include <dhcp4/tests/dhcp4_client.h> +#include <boost/shared_ptr.hpp> +#include <stats/stats_mgr.h> + +using namespace isc; +using namespace isc::asiolink; +using namespace isc::data; +using namespace isc::dhcp; +using namespace isc::dhcp::test; +using namespace isc::stats; + + +namespace { + +/// @brief Set of JSON configuration(s) used throughout the Host tests. +/// +/// - Configuration 0: +/// - Used for testing global host reservations +/// - 5 global reservations +/// - 1 subnet: 10.0.0.0/24 +const char* CONFIGS[] = { + // Configuration 0 + // 1 subnet, global only, + // global reservations for different identifier types + "{ \"interfaces-config\": {\n" + " \"interfaces\": [ \"*\" ]\n" + "},\n" + "\"host-reservation-identifiers\": [ \"circuit-id\", \"hw-address\",\n" + " \"duid\", \"client-id\" ],\n" + "\"reservations\": [ \n" + "{\n" + " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n" + " \"hostname\": \"hw-host-dynamic\"\n" + "},\n" + "{\n" + " \"hw-address\": \"01:02:03:04:05:06\",\n" + " \"hostname\": \"hw-host-fixed-out-of-range\",\n" + " \"ip-address\": \"192.0.1.77\"\n" + "},\n" + "{\n" + " \"hw-address\": \"02:02:03:04:05:06\",\n" + " \"hostname\": \"hw-host-fixed-in-range\",\n" + " \"ip-address\": \"10.0.0.77\"\n" + "},\n" + "{\n" + " \"duid\": \"01:02:03:04:05\",\n" + " \"hostname\": \"duid-host\"\n" + "},\n" + "{\n" + " \"circuit-id\": \"'charter950'\",\n" + " \"hostname\": \"circuit-id-host\"\n" + "},\n" + "{\n" + " \"client-id\": \"01:11:22:33:44:55:66\",\n" + " \"hostname\": \"client-id-host\"\n" + "}\n" + "],\n" + "\"valid-lifetime\": 600,\n" + "\"subnet4\": [ { \n" + " \"subnet\": \"10.0.0.0/24\",\n" + " \"id\": 10,\n" + " \"reservations-global\": true,\n" + " \"reservations-in-subnet\": false,\n" + " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ]\n" + "} ]\n" + "}\n" + , + // Configuration 1 global vs in-subnet + // 2 subnets, one default reservations flags (aka in-subnet), + // one reservations flags global only + // Host reservations for the same client, one global, one in each subnet + "{ \"interfaces-config\": {\n" + " \"interfaces\": [ \"*\" ]\n" + "},\n" + "\"valid-lifetime\": 600,\n" + "\"reservations\": [ \n" + "{\n" + " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n" + " \"hostname\": \"global-host\"\n" + "}\n" + "],\n" + "\"subnet4\": [\n" + " {\n" + " \"subnet\": \"10.0.0.0/24\", \n" + " \"id\": 10, \n" + " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],\n" + " \"interface\": \"eth0\",\n" + " \"reservations\": [ \n" + " {\n" + " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n" + " \"hostname\": \"subnet-10-host\"\n" + " }]\n" + " },\n" + " {\n" + " \"subnet\": \"192.0.2.0/26\", \n" + " \"id\": 20," + " \"pools\": [ { \"pool\": \"192.0.2.10-192.0.2.63\" } ],\n" + " \"interface\": \"eth1\",\n" + " \"reservations-global\": true,\n" + " \"reservations-in-subnet\": false,\n" + " \"reservations\": [ \n" + " {\n" + " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n" + " \"hostname\": \"subnet-20-host\"\n" + " }]\n" + " }\n" + "]\n" + "}\n" + , + // Configuration 2 global and in-subnet with out-of-pool + "{ \"interfaces-config\": {\n" + " \"interfaces\": [ \"*\" ]\n" + "},\n" + "\"valid-lifetime\": 600,\n" + "\"reservations\": [ \n" + "{\n" + " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n" + " \"hostname\": \"global-host\"\n" + "}\n" + "],\n" + "\"subnet4\": [\n" + " {\n" + " \"subnet\": \"10.0.0.0/24\", \n" + " \"id\": 10, \n" + " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],\n" + " \"interface\": \"eth0\",\n" + " \"reservations-global\": false,\n" + " \"reservations-in-subnet\": true,\n" + " \"reservations-out-of-pool\": true,\n" + " \"reservations\": [ \n" + " {\n" + " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n" + " \"hostname\": \"subnet-10-host\",\n" + " \"ip-address\": \"10.0.0.105\"\n" + " }]\n" + " }\n" + "]\n" + "}\n" + , + // Configuration 3 global and in-subnet + "{ \"interfaces-config\": {\n" + " \"interfaces\": [ \"*\" ]\n" + "},\n" + "\"valid-lifetime\": 600,\n" + "\"reservations\": [ \n" + "{\n" + " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n" + " \"hostname\": \"global-host\"\n" + "}\n" + "],\n" + "\"subnet4\": [\n" + " {\n" + " \"subnet\": \"10.0.0.0/24\", \n" + " \"id\": 10, \n" + " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],\n" + " \"interface\": \"eth0\",\n" + " \"reservations-global\": false,\n" + " \"reservations-in-subnet\": true,\n" + " \"reservations-out-of-pool\": false,\n" + " \"reservations\": [ \n" + " {\n" + " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n" + " \"hostname\": \"subnet-10-host\",\n" + " \"ip-address\": \"10.0.0.105\"\n" + " }]\n" + " }\n" + "]\n" + "}\n" + , + + // Configuration 4 client-class reservation in global, shared network + // and client-class guarded pools. + "{ \"interfaces-config\": {\n" + " \"interfaces\": [ \"*\" ]\n" + "},\n" + "\"client-classes\": [" + "{" + " \"name\": \"reserved_class\"" + "}," + "{" + " \"name\": \"unreserved_class\"," + " \"test\": \"not member('reserved_class')\"" + "}" + "],\n" + "\"reservations-global\": true,\n" + "\"reservations-in-subnet\": false,\n" + "\"valid-lifetime\": 600,\n" + "\"reservations\": [ \n" + "{\n" + " \"hw-address\": \"aa:bb:cc:dd:ee:fe\",\n" + " \"client-classes\": [ \"reserved_class\" ]\n" + "}\n" + "],\n" + "\"shared-networks\": [{" + " \"name\": \"frog\",\n" + " \"subnet4\": [\n" + " {\n" + " \"subnet\": \"10.0.0.0/24\", \n" + " \"id\": 10, \n" + " \"pools\": [" + " {" + " \"pool\": \"10.0.0.10-10.0.0.11\"," + " \"client-class\": \"reserved_class\"" + " }" + " ],\n" + " \"interface\": \"eth0\"\n" + " },\n" + " {\n" + " \"subnet\": \"192.0.3.0/24\", \n" + " \"id\": 11," + " \"pools\": [" + " {" + " \"pool\": \"192.0.3.10-192.0.3.11\"," + " \"client-class\": \"unreserved_class\"" + " }" + " ],\n" + " \"interface\": \"eth0\"\n" + " }\n" + " ]\n" + "}]\n" + "}", + + // Configuration 5 client-class reservation in global, shared network + // and client-class guarded subnets. + "{ \"interfaces-config\": {\n" + " \"interfaces\": [ \"*\" ]\n" + "},\n" + "\"client-classes\": [" + "{" + " \"name\": \"reserved_class\"" + "}," + "{" + " \"name\": \"unreserved_class\"," + " \"test\": \"not member('reserved_class')\"" + "}" + "],\n" + "\"reservations-global\": true,\n" + "\"reservations-in-subnet\": false,\n" + "\"valid-lifetime\": 600,\n" + "\"reservations\": [ \n" + "{\n" + " \"hw-address\": \"aa:bb:cc:dd:ee:fe\",\n" + " \"client-classes\": [ \"reserved_class\" ]\n" + "}\n" + "],\n" + "\"shared-networks\": [{" + " \"name\": \"frog\",\n" + " \"subnet4\": [\n" + " {\n" + " \"subnet\": \"10.0.0.0/24\", \n" + " \"id\": 10, \n" + " \"client-class\": \"reserved_class\"," + " \"pools\": [" + " {" + " \"pool\": \"10.0.0.10-10.0.0.10\"" + " }" + " ],\n" + " \"interface\": \"eth0\"\n" + " },\n" + " {\n" + " \"subnet\": \"192.0.3.0/24\", \n" + " \"id\": 11," + " \"client-class\": \"unreserved_class\"," + " \"pools\": [" + " {" + " \"pool\": \"192.0.3.10-192.0.3.10\"" + " }" + " ],\n" + " \"interface\": \"eth0\"\n" + " }\n" + " ]\n" + "}]\n" + "}", + + // Configuration 6 client-class reservation and client-class guarded pools. + "{ \"interfaces-config\": {\n" + " \"interfaces\": [ \"*\" ]\n" + "},\n" + "\"client-classes\": [" + "{" + " \"name\": \"reserved_class\"" + "}," + "{" + " \"name\": \"unreserved_class\"," + " \"test\": \"not member('reserved_class')\"" + "}" + "],\n" + "\"valid-lifetime\": 600,\n" + "\"subnet4\": [\n" + " {\n" + " \"subnet\": \"10.0.0.0/24\", \n" + " \"id\": 10, \n" + " \"reservations\": [{ \n" + " \"hw-address\": \"aa:bb:cc:dd:ee:fe\",\n" + " \"client-classes\": [ \"reserved_class\" ]\n" + " }],\n" + " \"pools\": [" + " {" + " \"pool\": \"10.0.0.10-10.0.0.11\"," + " \"client-class\": \"reserved_class\"" + " }," + " {" + " \"pool\": \"10.0.0.20-10.0.0.21\"," + " \"client-class\": \"unreserved_class\"" + " }" + " ],\n" + " \"interface\": \"eth0\"\n" + " }\n" + "]\n" + "}", + + // Configuration 7 multiple reservations for the same IP address. + "{ \"interfaces-config\": {\n" + " \"interfaces\": [ \"*\" ]\n" + "},\n" + "\"valid-lifetime\": 600,\n" + "\"ip-reservations-unique\": false,\n" + "\"host-reservation-identifiers\": [ \"hw-address\" ],\n" + "\"subnet4\": [\n" + " {\n" + " \"subnet\": \"10.0.0.0/24\",\n" + " \"id\": 10,\n" + " \"reservations\": [\n" + " { \n" + " \"hw-address\": \"aa:bb:cc:dd:ee:fe\",\n" + " \"ip-address\": \"10.0.0.123\"\n" + " },\n" + " { \n" + " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n" + " \"ip-address\": \"10.0.0.123\"\n" + " }\n" + " ],\n" + " \"pools\": [\n" + " {\n" + " \"pool\": \"10.0.0.10-10.0.0.255\"\n" + " }\n" + " ],\n" + " \"interface\": \"eth0\"\n" + " }\n" + "]\n" + "}", + + // Configuration 8 both global and in-subnet + // 2 subnets, one default reservations flags (aka in-subnet), + // one reservations flags global and in-subnet. + // Host reservations for the same client, one global, one in each subnet + "{ \"interfaces-config\": {\n" + " \"interfaces\": [ \"*\" ]\n" + "},\n" + "\"valid-lifetime\": 600,\n" + "\"reservations\": [ \n" + "{\n" + " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n" + " \"hostname\": \"global-host\"\n" + "}\n" + "],\n" + "\"subnet4\": [\n" + " {\n" + " \"subnet\": \"10.0.0.0/24\", \n" + " \"id\": 10, \n" + " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],\n" + " \"interface\": \"eth0\",\n" + " \"reservations\": [ \n" + " {\n" + " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n" + " \"hostname\": \"subnet-10-host\"\n" + " }]\n" + " },\n" + " {\n" + " \"subnet\": \"192.0.2.0/26\", \n" + " \"id\": 20," + " \"pools\": [ { \"pool\": \"192.0.2.10-192.0.2.63\" } ],\n" + " \"interface\": \"eth1\",\n" + " \"reservations-global\": true,\n" + " \"reservations-in-subnet\": true,\n" + " \"reservations-out-of-pool\": false,\n" + " \"reservations\": [ \n" + " {\n" + " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n" + " \"hostname\": \"subnet-20-host\"\n" + " }]\n" + " }\n" + "]\n" + "}\n" +}; + +/// @brief Test fixture class for testing global v4 reservations. +class HostTest : public Dhcpv4SrvTest { +public: + + /// @brief Constructor. + /// + /// Sets up fake interfaces. + HostTest() + : Dhcpv4SrvTest(), + iface_mgr_test_config_(true) { + // Let's wipe all existing statistics. + isc::stats::StatsMgr::instance().removeAll(); + } + + /// @brief Destructor. + /// + /// Cleans up statistics after the test. + ~HostTest() { + // Let's wipe all existing statistics. + isc::stats::StatsMgr::instance().removeAll(); + } + + /// @brief Interface Manager's fake configuration control. + IfaceMgrTestConfig iface_mgr_test_config_; + + /// @brief Conducts DORA exchange and checks assigned address and hostname + /// + /// If expected_host is empty, the test expects the hostname option to not + /// be assigned. + /// + /// @param config configuration to be used + /// @param client reference to a client instance + /// @param expected_host expected hostname to be assigned (may be empty) + /// @param expected_addr expected address to be assigned + void runDoraTest(const std::string& config, Dhcp4Client& client, + const std::string& expected_host, + const std::string& expected_addr, + const std::string& requested_addr = "") { + + // Configure DHCP server. + ASSERT_NO_FATAL_FAILURE(configure(config, *client.getServer())); + client.requestOptions(DHO_HOST_NAME); + + // Perform 4-way exchange with the server but to not request any + // specific address in the DHCPDISCOVER message. + boost::shared_ptr<IOAddress> hint; + if (!requested_addr.empty()) { + hint = boost::make_shared<IOAddress>(requested_addr); + } + ASSERT_NO_THROW(client.doDORA(hint)); + + // Make sure that the server responded. + ASSERT_TRUE(client.getContext().response_); + Pkt4Ptr resp = client.getContext().response_; + + // Make sure that the server has responded with DHCPACK. + ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType())); + + // Fetch the hostname option + OptionStringPtr hostname = boost::dynamic_pointer_cast< + OptionString>(resp->getOption(DHO_HOST_NAME)); + + if (expected_host.empty()) { + ASSERT_FALSE(hostname); + } else { + ASSERT_TRUE(hostname); + EXPECT_EQ(expected_host, hostname->getValue()); + } + + EXPECT_EQ(client.config_.lease_.addr_.toText(), expected_addr); + } + + /// @brief Test pool or subnet selection using global class reservation. + /// + /// Verifies that client class specified in the global reservation + /// may be used to influence pool or subnet selection. + /// + /// @param config_idx Index of the server configuration from the + /// @c CONFIGS array. + /// @param first_address Address to be allocated from the pool having + /// a reservation. + /// @param second_address Address to be allocated from the pool not + /// having a reservation. + void testGlobalClassSubnetPoolSelection(const int config_idx, + const std::string& first_address = "10.0.0.10", + const std::string& second_address = "192.0.3.10") { + Dhcp4Client client_resrv(Dhcp4Client::SELECTING); + + // Use HW address for which we have host reservation including + // client class. + client_resrv.setHWAddress("aa:bb:cc:dd:ee:fe"); + client_resrv.setIfaceName("eth0"); + client_resrv.setIfaceIndex(ETH0_INDEX); + + ASSERT_NO_FATAL_FAILURE(configure(CONFIGS[config_idx], *client_resrv.getServer())); + + // This client should be given an address from the 10.0.0.0/24 pool. + // Let's use the 192.0.3.10 as a hint to make sure that the server + // refuses allocating it and uses the sole pool available for this + // client. + ASSERT_NO_THROW(client_resrv.doDORA(boost::make_shared<IOAddress>(second_address))); + ASSERT_TRUE(client_resrv.getContext().response_); + auto resp = client_resrv.getContext().response_; + ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType())); + EXPECT_EQ(first_address, resp->getYiaddr().toText()); + + // This client has no reservation and therefore should be + // assigned to the unreserved_class and be given an address + // from the other pool. + Dhcp4Client client_no_resrv(client_resrv.getServer(), Dhcp4Client::SELECTING); + client_no_resrv.setHWAddress("aa:bb:cc:dd:ee:ff"); + client_no_resrv.setIfaceName("eth0"); + client_no_resrv.setIfaceIndex(ETH0_INDEX); + + // Let's use the address of 10.0.0.10 as a hint to make sure that the + // server refuses it in favor of the 192.0.3.10. + ASSERT_NO_THROW(client_no_resrv.doDORA(boost::make_shared<IOAddress>(first_address))); + ASSERT_TRUE(client_no_resrv.getContext().response_); + resp = client_no_resrv.getContext().response_; + ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType())); + EXPECT_EQ(second_address, resp->getYiaddr().toText()); + } + + /// @brief Test that two clients having reservations for the same IP + /// address are offered the reserved lease. + /// + /// This test verifies the case when two clients have reservations for + /// the same IP address. The first client sends DHCPDICOVER and is + /// offered the reserved address. At the same time, the second client + /// having the reservation for the same IP address performs 4-way + /// exchange using the reserved address as a hint in DHCPDISCOVER. + /// The client gets the lease for this address. This test verifies + /// that the allocation engine correctly identifies that the second + /// client has a reservation for this address. In order to verify + /// that the allocation engine must fetch all reservations for the + /// reserved address and verifies that one of them belongs to the + /// second client. + /// + /// @param hw_address1 Hardware address of the first client having + /// the reservation. + /// @param hw_address2 Hardware address of the second client having + /// the reservation. + void testMultipleClientsRace(const std::string& hw_address1, + const std::string& hw_address2) { + // Create first client having the reservation. + Dhcp4Client client1(Dhcp4Client::SELECTING); + client1.setHWAddress(hw_address1); + + // Configure the server. + ASSERT_NO_FATAL_FAILURE(configure(CONFIGS[7], *client1.getServer())); + + // Sends DHCPDISCOVER and make sure the client is offered the + // reserved IP address. + client1.doDiscover(boost::make_shared<IOAddress>("10.0.0.123")); + ASSERT_TRUE(client1.getContext().response_); + Pkt4Ptr resp = client1.getContext().response_; + ASSERT_EQ(DHCPOFFER, static_cast<int>(resp->getType())); + EXPECT_EQ("10.0.0.123", resp->getYiaddr().toText()); + + // Create the second client matching the second reservation for + // the given IP address. + Dhcp4Client client2(client1.getServer(), Dhcp4Client::SELECTING); + client2.setHWAddress(hw_address2); + + // Make sure that the second client gets the reserved lease. + client2.doDORA(boost::make_shared<IOAddress>("10.0.0.123")); + ASSERT_TRUE(client2.getContext().response_); + resp = client2.getContext().response_; + ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType())); + EXPECT_EQ("10.0.0.123", resp->getYiaddr().toText()); + } +}; + +// Verifies that a client, which fails to match to a global +// reservation, still gets a dynamic address when subnet reservations +// flags are global only. +TEST_F(HostTest, globalHardwareNoMatch) { + Dhcp4Client client(Dhcp4Client::SELECTING); + + client.setHWAddress("99:99:99:99:99:99"); + runDoraTest(CONFIGS[0], client, "", "10.0.0.10"); +} + +// Verifies that a client, that matches to a global hostname +// reservation, gets both the hostname and a dynamic address, +// when the subnet reservations flags are global only. +TEST_F(HostTest, globalHardwareDynamicAddress) { + Dhcp4Client client(Dhcp4Client::SELECTING); + + client.setHWAddress("aa:bb:cc:dd:ee:ff"); + runDoraTest(CONFIGS[0], client, "hw-host-dynamic", "10.0.0.10"); +} + +// Verifies that a client matched to a global in-subnet address reservation +// gets both the hostname and the reserved address when the subnet reservations +// flags are global only. +TEST_F(HostTest, globalHardwareFixedAddressInRange) { + Dhcp4Client client(Dhcp4Client::SELECTING); + + client.setHWAddress("02:02:03:04:05:06"); + runDoraTest(CONFIGS[0], client, "hw-host-fixed-in-range", "10.0.0.77"); +} + +// Verifies that a client matched to a global out-of-range address reservation +// gets the hostname and a dynamic address when the subnet reservations +// flags are global only. +TEST_F(HostTest, globalHardwareFixedAddressOutOfRange) { + Dhcp4Client client(Dhcp4Client::SELECTING); + + client.setHWAddress("01:02:03:04:05:06"); + runDoraTest(CONFIGS[0], client, "hw-host-fixed-out-of-range", "10.0.0.10"); +} + +// Verifies that a client can be matched to a global reservation by DUID +TEST_F(HostTest, globalDuid) { + Dhcp4Client client(Dhcp4Client::SELECTING); + + // Set hw address to a none-matching value + client.setHWAddress("99:99:99:99:99:99"); + + // - FF is a client identifier type for DUID, + // - 45454545 - represents 4 bytes for IAID + // - 01:02:03:04:05 - is an actual DUID for which there is a + client.includeClientId("FF:45:45:45:45:01:02:03:04:05"); + + runDoraTest(CONFIGS[0], client, "duid-host", "10.0.0.10"); +} + +// Verifies that a client can be matched to a global reservation by circuit-id +TEST_F(HostTest, globalCircuitId) { + Dhcp4Client client(Dhcp4Client::SELECTING); + + // Set hw address to a none-matching value + client.setHWAddress("99:99:99:99:99:99"); + + // Use relay agent so as the circuit-id can be inserted. + client.useRelay(true, IOAddress("10.0.0.1"), IOAddress("10.0.0.2")); + + // Set the circuit id + client.setCircuitId("charter950"); + + runDoraTest(CONFIGS[0], client, "circuit-id-host", "10.0.0.10"); +} + +// Verifies that a client can be matched to a global reservation by client-id +TEST_F(HostTest, globalClientID) { + Dhcp4Client client(Dhcp4Client::SELECTING); + + // Set hw address to a none-matching value + client.setHWAddress("99:99:99:99:99:99"); + + // - 01 is a client identifier type for CLIENT_ID, + // - 11:22:33:44:55:66 - is an actual DUID for which there is a + client.includeClientId("01:11:22:33:44:55:66"); + + runDoraTest(CONFIGS[0], client, "client-id-host", "10.0.0.10"); +} + +// Verifies that even when a matching global reservation exists, +// client will get a subnet scoped reservation, when subnet +// reservations flags are default +TEST_F(HostTest, defaultOverGlobal) { + Dhcp4Client client(Dhcp4Client::SELECTING); + + // Hardware address matches all reservations + client.setHWAddress("aa:bb:cc:dd:ee:ff"); + + // Subnet 10 uses default reservations flags (i.e. in-subnet), so its + // reservation should be used, rather than global. + runDoraTest(CONFIGS[1], client, "subnet-10-host", "10.0.0.10"); +} + +// Verifies that when there are matching reservations at +// both the global and subnet levels, client will be matched +// to the global reservation, when subnet reservations flags +// are global only. +TEST_F(HostTest, globalOverSubnet) { + Dhcp4Client client(Dhcp4Client::SELECTING); + + // Hardware address matches all reservations + client.setHWAddress("aa:bb:cc:dd:ee:ff"); + + // Change to subnet 20 + client.setIfaceName("eth1"); + client.setIfaceIndex(ETH1_INDEX); + + // Subnet 20 uses global only reservations flags, so the global + // reservation should be used, rather than the subnet one. + runDoraTest(CONFIGS[1], client, "global-host", "192.0.2.10"); +} + +// Verifies that when there are matching reservations at +// both the global and subnet levels, client will be matched +// to the subnet reservation, when subnet reservations flags +// are in-subnet and out-of-pool. +TEST_F(HostTest, outOfPoolOverGlobal) { + Dhcp4Client client(Dhcp4Client::SELECTING); + + // Hardware address matches all reservations + client.setHWAddress("aa:bb:cc:dd:ee:ff"); + + // Subnet 10 uses in-subnet and out-of-pool reservations flags, + // so its reservation should be used, rather than global. + runDoraTest(CONFIGS[2], client, "subnet-10-host", "10.0.0.105"); +} + +// Verifies that when there are matching reservations at +// both the global and subnet levels, client will be matched +// to the subnet reservation, when subnet reservations flags +// are in-subnet only. +TEST_F(HostTest, allOverGlobal) { + Dhcp4Client client(Dhcp4Client::SELECTING); + + // Hardware address matches all reservations + client.setHWAddress("aa:bb:cc:dd:ee:ff"); + + // Subnet 10 uses default reservations flags (i.e. in-subnet), so its + // reservation should be used, rather than global. + runDoraTest(CONFIGS[3], client, "subnet-10-host", "10.0.0.105"); +} + +// Verifies that when there are matching reservations at +// both the global and subnet levels, client will be matched +// to the subnet reservation, when subnet reservations flags +// are global and in-subnet, i.e. the subnet has the preference. +TEST_F(HostTest, subnetOverGlobal) { + Dhcp4Client client(Dhcp4Client::SELECTING); + + // Hardware address matches all reservations + client.setHWAddress("aa:bb:cc:dd:ee:ff"); + + // Change to subnet 20 + client.setIfaceName("eth1"); + client.setIfaceIndex(ETH1_INDEX); + + // Subnet 20 uses both global and in-subnet reservations flags, + // so the subnet reservation has the preference. + runDoraTest(CONFIGS[8], client, "subnet-20-host", "192.0.2.10"); +} + +// Verifies that client class specified in the global reservation +// may be used to influence pool selection. +TEST_F(HostTest, clientClassGlobalPoolSelection) { + ASSERT_NO_FATAL_FAILURE(testGlobalClassSubnetPoolSelection(4)); +} + +// Verifies that client class specified in the global reservation +// may be used to influence subnet selection within shared network. +TEST_F(HostTest, clientClassGlobalSubnetSelection) { + ASSERT_NO_FATAL_FAILURE(testGlobalClassSubnetPoolSelection(5)); +} + +// Verifies that client class specified in the reservation may be +// used to influence pool selection within a subnet. +TEST_F(HostTest, clientClassPoolSelection) { + ASSERT_NO_FATAL_FAILURE(testGlobalClassSubnetPoolSelection(6, "10.0.0.10", "10.0.0.20")); +} + +// Verifies that if the server is configured to allow for specifying +// multiple reservations for the same IP address the first client +// matching the reservation will be given this address. The second +// client will be given a different lease. +TEST_F(HostTest, firstClientGetsReservedAddress) { + // Create a client which has MAC address matching the reservation. + Dhcp4Client client1(Dhcp4Client::SELECTING); + client1.setHWAddress("aa:bb:cc:dd:ee:fe"); + // Do 4-way exchange for this client to get the reserved address. + runDoraTest(CONFIGS[7], client1, "", "10.0.0.123"); + + // Create another client that has a reservation for the same + // IP address. + Dhcp4Client client2(client1.getServer(), Dhcp4Client::SELECTING); + client2.setHWAddress("aa:bb:cc:dd:ee:ff"); + // Do 4-way exchange with client2. + ASSERT_NO_THROW(client2.doDORA()); + + // Make sure that the server responded with DHCPACK. + ASSERT_TRUE(client2.getContext().response_); + Pkt4Ptr resp = client2.getContext().response_; + ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType())); + + // Even though the client has reservation for this address the + // server should not assign this address because another client + // has taken it already. + EXPECT_NE("10.0.0.123", resp->getYiaddr().toText()); + // Ensure stats are being recorded for HR conflicts + ObservationPtr subnet_conflicts = StatsMgr::instance().getObservation( + "subnet[10].v4-reservation-conflicts"); + ASSERT_TRUE(subnet_conflicts); + ASSERT_EQ(1, subnet_conflicts->getInteger().first); + subnet_conflicts = StatsMgr::instance().getObservation("v4-reservation-conflicts"); + ASSERT_TRUE(subnet_conflicts); + ASSERT_EQ(1, subnet_conflicts->getInteger().first); + + // If the client1 releases the reserved lease, the client2 should acquire it. + ASSERT_NO_THROW(client1.doRelease()); + + // Client2 attempts to renew the currently used lease, but should get the + // DHCPNAK. + client2.setState(Dhcp4Client::RENEWING); + ASSERT_NO_THROW(client2.doRequest()); + resp = client2.getContext().response_; + ASSERT_TRUE(resp); + ASSERT_EQ(DHCPNAK, static_cast<int>(resp->getType())); + + // The client falls back to 4-way exchange and gets the reserved address. + client2.setState(Dhcp4Client::SELECTING); + ASSERT_NO_THROW(client2.doDORA()); + resp = client2.getContext().response_; + ASSERT_TRUE(resp); + ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType())); + EXPECT_EQ("10.0.0.123", resp->getYiaddr().toText()); +} + +// This test verifies the case when two clients have reservations for +// the same IP address. The first client sends DHCPDICOVER and is +// offered the reserved address. At the same time, the second client +// having the reservation for the same IP address performs 4-way +// exchange using the reserved address as a hint in DHCPDISCOVER. +// The client gets the lease for this address. This test verifies +// that the allocation engine correctly identifies that the second +// client has a reservation for this address. In order to verify +// that the allocation engine must fetch all reservations for the +// reserved address and verifies that one of them belongs to the +// second client. +TEST_F(HostTest, multipleClientsRace1) { + ASSERT_NO_FATAL_FAILURE(testMultipleClientsRace("aa:bb:cc:dd:ee:fe", + "aa:bb:cc:dd:ee:ff")); +} + +// This is a second variant of the multipleClientsRace1. The test is almost +// the same but the client matching the second reservation sends DHCPDISCOVER +// first and then the client having the first reservation performs 4-way +// exchange. This is to ensure that the order in which reservations are +// defined does not matter. +TEST_F(HostTest, multipleClientsRace2) { + ASSERT_NO_FATAL_FAILURE(testMultipleClientsRace("aa:bb:cc:dd:ee:ff", + "aa:bb:cc:dd:ee:fe")); +} + +} // end of anonymous namespace |