diff --git a/src/bin/dhcp4/dhcp4_srv.cc b/src/bin/dhcp4/dhcp4_srv.cc index e44bfb17cc..8d21e27586 100644 --- a/src/bin/dhcp4/dhcp4_srv.cc +++ b/src/bin/dhcp4/dhcp4_srv.cc @@ -186,6 +186,17 @@ Dhcpv4Exchange::Dhcpv4Exchange(const AllocEnginePtr& alloc_engine, } } + // Global host reservations are independent of a selected subnet. If the + // global reservations contain client classes we should use them in case + // they are meant to affect pool selection. + auto global_host = context_->globalHost(); + if (global_host && !global_host->getClientClasses4().empty()) { + // Previously evaluated classes must be ignored because having new + // classes fetched from the hosts db may eliminate some of them. + query->classes_.clear(); + setReservedClientClasses(); + } + // Set KNOWN builtin class if something was found, UNKNOWN if not. if (!context_->hosts_.empty()) { query->addClass("KNOWN"); @@ -2784,8 +2795,12 @@ Dhcpv4Srv::processDiscover(Pkt4Ptr& discover) { // Adding any other options makes sense only when we got the lease. if (!ex.getResponse()->getYiaddr().isV4Zero()) { - // Assign reserved classes. - ex.setReservedClientClasses(); + // If this is global reservation we have already fetched it and + // evaluated the classes. + if (!ex.getContext()->globalHost()) { + // Assign reserved classes. + ex.setReservedClientClasses(); + } // Required classification requiredClassify(ex); @@ -2852,8 +2867,12 @@ Dhcpv4Srv::processRequest(Pkt4Ptr& request, AllocEngine::ClientContext4Ptr& cont // Adding any other options makes sense only when we got the lease. if (!response->getYiaddr().isV4Zero()) { - // Assign reserved classes. - ex.setReservedClientClasses(); + // If this is global reservation we have already fetched it and + // evaluated the classes. + if (!ex.getContext()->globalHost()) { + // Assign reserved classes. + ex.setReservedClientClasses(); + } // Required classification requiredClassify(ex); @@ -3166,7 +3185,11 @@ Dhcpv4Srv::processInform(Pkt4Ptr& inform) { Pkt4Ptr ack = ex.getResponse(); - ex.setReservedClientClasses(); + // If this is global reservation we have already fetched it and + // evaluated the classes. + if (!ex.getContext()->globalHost()) { + ex.setReservedClientClasses(); + } requiredClassify(ex); buildCfgOptionList(ex); diff --git a/src/bin/dhcp4/tests/host_unittest.cc b/src/bin/dhcp4/tests/host_unittest.cc index 9b1aa6c904..99672af753 100644 --- a/src/bin/dhcp4/tests/host_unittest.cc +++ b/src/bin/dhcp4/tests/host_unittest.cc @@ -1,4 +1,4 @@ -// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2018-2020 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 @@ -166,6 +166,109 @@ const char* CONFIGS[] = { " }\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" + "\"reservation-mode\": \"global\"," + "\"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," + " \"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" + "\"reservation-mode\": \"global\"," + "\"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," + " \"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" + "}" }; /// @brief Test fixture class for testing global v4 reservations. @@ -206,7 +309,8 @@ public: /// @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& expected_addr, + const std::string& requested_addr = "") { // Configure DHCP server. ASSERT_NO_FATAL_FAILURE(configure(config, *client.getServer())); @@ -214,7 +318,11 @@ public: // Perform 4-way exchange with the server but to not request any // specific address in the DHCPDISCOVER message. - ASSERT_NO_THROW(client.doDORA()); + boost::shared_ptr hint; + if (!requested_addr.empty()) { + hint = boost::make_shared(requested_addr); + } + ASSERT_NO_THROW(client.doDORA(hint)); // Make sure that the server responded. ASSERT_TRUE(client.getContext().response_); @@ -237,7 +345,48 @@ public: 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. + void testGlobalClassSubnetPoolSelection(const int config_idx) { + 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"); + + 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("192.0.3.10"))); + ASSERT_TRUE(client_resrv.getContext().response_); + auto resp = client_resrv.getContext().response_; + ASSERT_EQ(DHCPACK, static_cast(resp->getType())); + EXPECT_EQ("10.0.0.10", 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"); + + // 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("10.0.0.10"))); + ASSERT_TRUE(client_no_resrv.getContext().response_); + resp = client_no_resrv.getContext().response_; + ASSERT_EQ(DHCPACK, static_cast(resp->getType())); + EXPECT_EQ("192.0.3.10", resp->getYiaddr().toText()); + } }; // Verifies that a client, which fails to match to a global @@ -377,4 +526,16 @@ TEST_F(HostTest, allOverGlobal) { runDoraTest(CONFIGS[3], client, "subnet-10-host", "192.0.5.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)); +} + } // end of anonymous namespace