// Copyright (C) 2014-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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace isc; using namespace isc::asiolink; using namespace isc::data; using namespace isc::dhcp; using namespace isc::dhcp::test; using namespace isc::stats; using namespace isc::test; using namespace isc::util; namespace { /// @brief An allocator recording calls to @c initAfterConfigure. class InitRecordingAllocator : public IterativeAllocator { public: /// @brief Constructor. /// /// @param type specifies the type of allocated leases. /// @param subnet weak pointer to the subnet owning the allocator. InitRecordingAllocator(Lease::Type type, const WeakSubnetPtr& subnet) : IterativeAllocator(type, subnet), callcount_(0) { } /// @brief Increases the call count of this function. /// /// The call count can be later examined to check whether or not /// the function was called. virtual void initAfterConfigureInternal() { ++callcount_; }; /// @brief Call count of the @c initAllocatorsAfterConfigure. int callcount_; }; /// @brief Verifies that a set of subnets contains a given a subnet /// /// @param cfg_subnets set of subnets in which to look /// @param prefix prefix of the target subnet /// @param exp_subnet_id expected id of the target subnet /// @param exp_valid expected valid lifetime of the subnet /// @param exp_network pointer to the subnet's shared-network (if one) void checkMergedSubnet(CfgSubnets4& cfg_subnets, const std::string& prefix, const SubnetID exp_subnet_id, int exp_valid, SharedNetwork4Ptr exp_network) { // Look for the network by prefix. auto subnet = cfg_subnets.getByPrefix(prefix); ASSERT_TRUE(subnet) << "subnet: " << prefix << " not found"; // Make sure we have the one we expect. ASSERT_EQ(exp_subnet_id, subnet->getID()) << "subnet ID is wrong"; ASSERT_EQ(exp_valid, subnet->getValid().get()) << "subnet valid time is wrong"; SharedNetwork4Ptr shared_network; subnet->getSharedNetwork(shared_network); if (exp_network) { ASSERT_TRUE(shared_network) << " expected network: " << exp_network->getName() << " not found"; ASSERT_TRUE(shared_network == exp_network) << " networks do no match"; } else { ASSERT_FALSE(shared_network) << " unexpected network assignment: " << shared_network->getName(); } } // This test verifies that specific subnet can be retrieved by specifying // subnet identifier or subnet prefix. TEST(CfgSubnets4Test, getSpecificSubnet) { CfgSubnets4 cfg; // Create 3 subnets. Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, SubnetID(5))); Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3, SubnetID(8))); Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3, SubnetID(10))); // Store the subnets in a vector to make it possible to loop over // all configured subnets. std::vector subnets; subnets.push_back(subnet1); subnets.push_back(subnet2); subnets.push_back(subnet3); // Add all subnets to the configuration. for (auto subnet = subnets.cbegin(); subnet != subnets.cend(); ++subnet) { ASSERT_NO_THROW(cfg.add(*subnet)) << "failed to add subnet with id: " << (*subnet)->getID(); } // Iterate over all subnets and make sure they can be retrieved by // subnet identifier. for (auto subnet = subnets.rbegin(); subnet != subnets.rend(); ++subnet) { ConstSubnet4Ptr subnet_returned = cfg.getBySubnetId((*subnet)->getID()); ASSERT_TRUE(subnet_returned) << "failed to return subnet with id: " << (*subnet)->getID(); EXPECT_EQ((*subnet)->getID(), subnet_returned->getID()); EXPECT_EQ((*subnet)->toText(), subnet_returned->toText()); } // Repeat the previous test, but this time retrieve subnets by their // prefixes. for (auto subnet = subnets.rbegin(); subnet != subnets.rend(); ++subnet) { ConstSubnet4Ptr subnet_returned = cfg.getByPrefix((*subnet)->toText()); ASSERT_TRUE(subnet_returned) << "failed to return subnet with id: " << (*subnet)->getID(); EXPECT_EQ((*subnet)->getID(), subnet_returned->getID()); EXPECT_EQ((*subnet)->toText(), subnet_returned->toText()); } // Make sure that null pointers are returned for non-existing subnets. EXPECT_FALSE(cfg.getBySubnetId(SubnetID(123))); EXPECT_FALSE(cfg.getByPrefix("10.20.30.0/29")); } // This test verifies that a single subnet can be removed from the configuration. TEST(CfgSubnets4Test, deleteSubnet) { CfgSubnets4 cfg; // Create 3 subnets. Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, SubnetID(5))); Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.3.0"), 26, 1, 2, 3, SubnetID(8))); Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.4.0"), 26, 1, 2, 3, SubnetID(10))); ASSERT_NO_THROW(cfg.add(subnet1)); ASSERT_NO_THROW(cfg.add(subnet2)); ASSERT_NO_THROW(cfg.add(subnet3)); // There should be three subnets. ASSERT_EQ(3, cfg.getAll()->size()); // We're going to remove the subnet #2. Let's make sure it exists before // we remove it. ASSERT_TRUE(cfg.getByPrefix("192.0.3.0/26")); // Remove the subnet and make sure it is gone. ASSERT_NO_THROW(cfg.del(subnet2)); ASSERT_EQ(2, cfg.getAll()->size()); EXPECT_FALSE(cfg.getByPrefix("192.0.3.0/26")); // Remove another subnet by ID. ASSERT_NO_THROW(cfg.del(subnet1->getID())); ASSERT_EQ(1, cfg.getAll()->size()); EXPECT_FALSE(cfg.getByPrefix("192.0.2.0/26")); } // This test verifies that replace a subnet works as expected. TEST(CfgSubnets4Test, replaceSubnet) { CfgSubnets4 cfg; // Create 3 subnets. Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.1.0"), 26, 1, 2, 100, SubnetID(10))); Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 100, SubnetID(2))); Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.3.0"), 26, 1, 2, 100, SubnetID(13))); ASSERT_NO_THROW(cfg.add(subnet1)); ASSERT_NO_THROW(cfg.add(subnet2)); ASSERT_NO_THROW(cfg.add(subnet3)); // There should be three subnets. ASSERT_EQ(3, cfg.getAll()->size()); // We're going to replace the subnet #2. Let's make sure it exists before // we replace it. ASSERT_TRUE(cfg.getByPrefix("192.0.3.0/26")); // Replace the subnet and make sure it was updated. Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.2.0"), 26, 10, 20, 1000, SubnetID(2))); Subnet4Ptr replaced = cfg.replace(subnet); ASSERT_TRUE(replaced); EXPECT_TRUE(replaced == subnet2); ASSERT_EQ(3, cfg.getAll()->size()); Subnet4Ptr returned = cfg.getSubnet(SubnetID(2)); ASSERT_TRUE(returned); EXPECT_TRUE(returned == subnet); // Restore. replaced = cfg.replace(replaced); ASSERT_TRUE(replaced); EXPECT_TRUE(replaced == subnet); ASSERT_EQ(3, cfg.getAll()->size()); returned = cfg.getSubnet(SubnetID(2)); ASSERT_TRUE(returned); EXPECT_TRUE(returned == subnet2); // Prefix conflict returns null. subnet.reset(new Subnet4(IOAddress("192.0.3.0"), 26, 10, 20, 1000, SubnetID(2))); replaced = cfg.replace(subnet); EXPECT_FALSE(replaced); returned = cfg.getSubnet(SubnetID(2)); ASSERT_TRUE(returned); EXPECT_TRUE(returned == subnet2); // Changing prefix works even it is highly not recommended. subnet.reset(new Subnet4(IOAddress("192.0.10.0"), 26, 10, 20, 1000, SubnetID(2))); replaced = cfg.replace(subnet); ASSERT_TRUE(replaced); EXPECT_TRUE(replaced == subnet2); returned = cfg.getSubnet(SubnetID(2)); ASSERT_TRUE(returned); EXPECT_TRUE(returned == subnet); } // This test verifies that subnets configuration is properly merged. TEST(CfgSubnets4Test, mergeSubnets) { // Create custom options dictionary for testing merge. We're keeping it // simple because they are more rigorous tests elsewhere. CfgOptionDefPtr cfg_def(new CfgOptionDef()); cfg_def->add((OptionDefinitionPtr(new OptionDefinition("one", 1, "isc", "string")))); Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.1.0"), 26, 1, 2, 100, SubnetID(1))); Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 100, SubnetID(2))); Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.3.0"), 26, 1, 2, 100, SubnetID(3))); Subnet4Ptr subnet4(new Subnet4(IOAddress("192.0.4.0"), 26, 1, 2, 100, SubnetID(4))); // Create the "existing" list of shared networks CfgSharedNetworks4Ptr networks(new CfgSharedNetworks4()); SharedNetwork4Ptr shared_network1(new SharedNetwork4("shared-network1")); networks->add(shared_network1); SharedNetwork4Ptr shared_network2(new SharedNetwork4("shared-network2")); networks->add(shared_network2); // Empty network pointer. SharedNetwork4Ptr no_network; // Add Subnets 1, 2 and 4 to shared networks. ASSERT_NO_THROW(shared_network1->add(subnet1)); ASSERT_NO_THROW(shared_network2->add(subnet2)); ASSERT_NO_THROW(shared_network2->add(subnet4)); // Create our "existing" configured subnets. CfgSubnets4 cfg_to; ASSERT_NO_THROW(cfg_to.add(subnet1)); ASSERT_NO_THROW(cfg_to.add(subnet2)); ASSERT_NO_THROW(cfg_to.add(subnet3)); ASSERT_NO_THROW(cfg_to.add(subnet4)); // Merge in an "empty" config. Should have the original config, // still intact. CfgSubnets4 cfg_from; ASSERT_NO_THROW(cfg_to.merge(cfg_def, networks, cfg_from)); // We should have all four subnets, with no changes. ASSERT_EQ(4, cfg_to.getAll()->size()); // Should be no changes to the configuration. ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "192.0.1.0/26", SubnetID(1), 100, shared_network1)); ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "192.0.2.0/26", SubnetID(2), 100, shared_network2)); ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "192.0.3.0/26", SubnetID(3), 100, no_network)); ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "192.0.4.0/26", SubnetID(4), 100, shared_network2)); // Fill cfg_from configuration with subnets. // subnet 1b updates subnet 1 but leaves it in network 1 with the same ID. Subnet4Ptr subnet1b(new Subnet4(IOAddress("192.0.10.0"), 26, 2, 3, 400, SubnetID(1))); subnet1b->setSharedNetworkName("shared-network1"); // Add generic option 1 to subnet 1b. std::string value("Yay!"); OptionPtr option(new Option(Option::V4, 1)); option->setData(value.begin(), value.end()); ASSERT_NO_THROW(subnet1b->getCfgOption()->add(option, false, false, "isc")); // subnet 3b updates subnet 3 with different ID and removes it // from network 2 Subnet4Ptr subnet3b(new Subnet4(IOAddress("192.0.3.0"), 26, 3, 4, 500, SubnetID(30))); // Now Add generic option 1 to subnet 3b. value = "Team!"; option.reset(new Option(Option::V4, 1)); option->setData(value.begin(), value.end()); ASSERT_NO_THROW(subnet3b->getCfgOption()->add(option, false, false, "isc")); // subnet 4b updates subnet 4 and moves it from network2 to network 1 Subnet4Ptr subnet4b(new Subnet4(IOAddress("192.0.4.0"), 26, 3, 4, 500, SubnetID(4))); subnet4b->setSharedNetworkName("shared-network1"); // subnet 5 is new and belongs to network 2 // Has two pools both with an option 1 Subnet4Ptr subnet5(new Subnet4(IOAddress("192.0.5.0"), 26, 1, 2, 300, SubnetID(5))); subnet5->setSharedNetworkName("shared-network2"); // Add pool 1 Pool4Ptr pool(new Pool4(IOAddress("192.0.5.10"), IOAddress("192.0.5.20"))); value = "POOLS"; option.reset(new Option(Option::V4, 1)); option->setData(value.begin(), value.end()); ASSERT_NO_THROW(pool->getCfgOption()->add(option, false, false, "isc")); subnet5->addPool(pool); // Add pool 2 pool.reset(new Pool4(IOAddress("192.0.5.30"), IOAddress("192.0.5.40"))); value ="RULE!"; option.reset(new Option(Option::V4, 1)); option->setData(value.begin(), value.end()); ASSERT_NO_THROW(pool->getCfgOption()->add(option, false, false, "isc")); subnet5->addPool(pool); // Add subnets to the merge from config. ASSERT_NO_THROW(cfg_from.add(subnet1b)); ASSERT_NO_THROW(cfg_from.add(subnet3b)); ASSERT_NO_THROW(cfg_from.add(subnet4b)); ASSERT_NO_THROW(cfg_from.add(subnet5)); // Merge again. ASSERT_NO_THROW(cfg_to.merge(cfg_def, networks, cfg_from)); ASSERT_EQ(5, cfg_to.getAll()->size()); // The subnet1 should be replaced by subnet1b. ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "192.0.10.0/26", SubnetID(1), 400, shared_network1)); // Let's verify that our option is there and populated correctly. auto subnet = cfg_to.getByPrefix("192.0.10.0/26"); auto desc = subnet->getCfgOption()->get("isc", 1); ASSERT_TRUE(desc.option_); OptionStringPtr opstr = boost::dynamic_pointer_cast(desc.option_); ASSERT_TRUE(opstr); EXPECT_EQ("Yay!", opstr->getValue()); // The subnet2 should not be affected because it was not present. ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "192.0.2.0/26", SubnetID(2), 100, shared_network2)); // subnet3 should be replaced by subnet3b and no longer assigned to a network. ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "192.0.3.0/26", SubnetID(30), 500, no_network)); // Let's verify that our option is there and populated correctly. subnet = cfg_to.getByPrefix("192.0.3.0/26"); desc = subnet->getCfgOption()->get("isc", 1); ASSERT_TRUE(desc.option_); opstr = boost::dynamic_pointer_cast(desc.option_); ASSERT_TRUE(opstr); EXPECT_EQ("Team!", opstr->getValue()); // subnet4 should be replaced by subnet4b and moved to network1. ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "192.0.4.0/26", SubnetID(4), 500, shared_network1)); // subnet5 should have been added to configuration. ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "192.0.5.0/26", SubnetID(5), 300, shared_network2)); // Let's verify that both pools have the proper options. subnet = cfg_to.getByPrefix("192.0.5.0/26"); const PoolPtr merged_pool = subnet->getPool(Lease::TYPE_V4, IOAddress("192.0.5.10")); ASSERT_TRUE(merged_pool); desc = merged_pool->getCfgOption()->get("isc", 1); opstr = boost::dynamic_pointer_cast(desc.option_); ASSERT_TRUE(opstr); EXPECT_EQ("POOLS", opstr->getValue()); const PoolPtr merged_pool2 = subnet->getPool(Lease::TYPE_V4, IOAddress("192.0.5.30")); ASSERT_TRUE(merged_pool2); desc = merged_pool2->getCfgOption()->get("isc", 1); opstr = boost::dynamic_pointer_cast(desc.option_); ASSERT_TRUE(opstr); EXPECT_EQ("RULE!", opstr->getValue()); } // This test verifies that it is possible to retrieve a subnet using an // IP address. TEST(CfgSubnets4Test, selectSubnetByCiaddr) { CfgSubnets4 cfg; // Create 3 subnets. Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, SubnetID(1))); Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3, SubnetID(2))); Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3, SubnetID(3))); // Make sure that initially the subnets don't exist. SubnetSelector selector; selector.ciaddr_ = IOAddress("192.0.2.0"); // Set some unicast local address to simulate a Renew. selector.local_address_ = IOAddress("10.0.0.100"); ASSERT_FALSE(cfg.selectSubnet(selector)); // Add one subnet and make sure it is returned. cfg.add(subnet1); selector.ciaddr_ = IOAddress("192.0.2.63"); EXPECT_EQ(subnet1, cfg.selectSubnet(selector)); // Add all other subnets. cfg.add(subnet2); cfg.add(subnet3); // Make sure they are returned for the appropriate addresses. selector.ciaddr_ = IOAddress("192.0.2.15"); EXPECT_EQ(subnet1, cfg.selectSubnet(selector)); selector.ciaddr_ = IOAddress("192.0.2.85"); EXPECT_EQ(subnet2, cfg.selectSubnet(selector)); selector.ciaddr_ = IOAddress("192.0.2.191"); EXPECT_EQ(subnet3, cfg.selectSubnet(selector)); // Also, make sure that the NULL pointer is returned if the subnet // cannot be found. selector.ciaddr_ = IOAddress("192.0.2.192"); EXPECT_FALSE(cfg.selectSubnet(selector)); } // This test verifies that it is possible to select a subnet by // matching an interface name. TEST(CfgSubnets4Test, selectSubnetByIface) { // The IfaceMgrTestConfig object initializes fake interfaces: // eth0, eth1 and lo on the configuration manager. The CfgSubnets4 // object uses interface names to select the appropriate subnet. IfaceMgrTestConfig config(true); CfgSubnets4 cfg; // Create 3 subnets. Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, SubnetID(1))); Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3, SubnetID(2))); Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3, SubnetID(3))); // No interface defined for subnet1 subnet2->setIface("lo"); subnet3->setIface("eth1"); cfg.add(subnet1); cfg.add(subnet2); cfg.add(subnet3); // Make sure that initially the subnets don't exist. SubnetSelector selector; // Set an interface to a name that is not defined in the config. // Subnet selection should fail. selector.iface_name_ = "eth0"; ASSERT_FALSE(cfg.selectSubnet(selector)); // Now select an interface name that matches. Selection should succeed // and return subnet3. selector.iface_name_ = "eth1"; Subnet4Ptr selected = cfg.selectSubnet(selector); ASSERT_TRUE(selected); EXPECT_EQ(subnet3, selected); } // This test verifies that it is possible to select subnet by interface // name specified on the shared network level. TEST(CfgSubnets4Test, selectSharedNetworkByIface) { // The IfaceMgrTestConfig object initializes fake interfaces: // eth0, eth1 and lo on the configuration manager. The CfgSubnets4 // object uses interface names to select the appropriate subnet. IfaceMgrTestConfig config(true); CfgSubnets4 cfg; // Create 3 subnets. Subnet4Ptr subnet1(new Subnet4(IOAddress("172.16.2.0"), 24, 1, 2, 3, SubnetID(1))); Subnet4Ptr subnet2(new Subnet4(IOAddress("10.1.2.0"), 24, 1, 2, 3, SubnetID(2))); Subnet4Ptr subnet3(new Subnet4(IOAddress("192.3.4.0"), 24, 1, 2, 3, SubnetID(3))); subnet2->setIface("lo"); cfg.add(subnet1); cfg.add(subnet2); cfg.add(subnet3); SharedNetwork4Ptr network(new SharedNetwork4("network_eth1")); network->setIface("eth1"); ASSERT_NO_THROW(network->add(subnet1)); ASSERT_NO_THROW(network->add(subnet2)); // Make sure that initially the subnets don't exist. SubnetSelector selector; // Set an interface to a name that is not defined in the config. // Subnet selection should fail. selector.iface_name_ = "eth0"; ASSERT_FALSE(cfg.selectSubnet(selector)); // Now select an interface name that matches. Selection should succeed // and return subnet3. selector.iface_name_ = "eth1"; Subnet4Ptr selected = cfg.selectSubnet(selector); ASSERT_TRUE(selected); SharedNetwork4Ptr network_returned; selected->getSharedNetwork(network_returned); ASSERT_TRUE(network_returned); EXPECT_EQ(network, network_returned); const Subnet4SimpleCollection* subnets_eth1 = network_returned->getAllSubnets(); EXPECT_EQ(2, subnets_eth1->size()); ASSERT_TRUE(network_returned->getSubnet(SubnetID(1))); ASSERT_TRUE(network_returned->getSubnet(SubnetID(2))); // Make sure that it is still possible to select subnet2 which is // outside of a shared network. selector.iface_name_ = "lo"; selected = cfg.selectSubnet(selector); ASSERT_TRUE(selected); EXPECT_EQ(2, selected->getID()); // Try selecting by eth1 again, but this time set subnet specific // interface name to eth0. Subnet selection should fail. selector.iface_name_ = "eth1"; subnet1->setIface("eth0"); subnet3->setIface("eth0"); selected = cfg.selectSubnet(selector); ASSERT_FALSE(selected); // It should be possible to select by eth0, though. selector.iface_name_ = "eth0"; selected = cfg.selectSubnet(selector); ASSERT_TRUE(selected); EXPECT_EQ(subnet1, selected); } // This test verifies that when the classification information is specified for // subnets, the proper subnets are returned by the subnet configuration. TEST(CfgSubnets4Test, selectSubnetByClasses) { CfgSubnets4 cfg; // Create 3 subnets. Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, SubnetID(1))); Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3, SubnetID(2))); Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3, SubnetID(3))); // Add them to the configuration. cfg.add(subnet1); cfg.add(subnet2); cfg.add(subnet3); SubnetSelector selector; selector.local_address_ = IOAddress("10.0.0.10"); selector.ciaddr_ = IOAddress("192.0.2.5"); EXPECT_EQ(subnet1, cfg.selectSubnet(selector)); selector.ciaddr_ = IOAddress("192.0.2.70"); EXPECT_EQ(subnet2, cfg.selectSubnet(selector)); selector.ciaddr_ = IOAddress("192.0.2.130"); EXPECT_EQ(subnet3, cfg.selectSubnet(selector)); ClientClasses client_classes; client_classes.insert("bar"); selector.client_classes_ = client_classes; // There are no class restrictions defined, so everything should work // as before selector.ciaddr_ = IOAddress("192.0.2.5"); EXPECT_EQ(subnet1, cfg.selectSubnet(selector)); selector.ciaddr_ = IOAddress("192.0.2.70"); EXPECT_EQ(subnet2, cfg.selectSubnet(selector)); selector.ciaddr_ = IOAddress("192.0.2.130"); EXPECT_EQ(subnet3, cfg.selectSubnet(selector)); // Now let's add client class restrictions. subnet1->allowClientClass("foo"); // Serve here only clients from foo class subnet2->allowClientClass("bar"); // Serve here only clients from bar class subnet3->allowClientClass("baz"); // Serve here only clients from baz class // The same check as above should result in client being served only in // bar class, i.e. subnet2. selector.ciaddr_ = IOAddress("192.0.2.5"); EXPECT_FALSE(cfg.selectSubnet(selector)); selector.ciaddr_ = IOAddress("192.0.2.70"); EXPECT_EQ(subnet2, cfg.selectSubnet(selector)); selector.ciaddr_ = IOAddress("192.0.2.130"); EXPECT_FALSE(cfg.selectSubnet(selector)); // Now let's check that client with wrong class is not supported. client_classes.clear(); client_classes.insert("some_other_class"); selector.client_classes_ = client_classes; selector.ciaddr_ = IOAddress("192.0.2.5"); EXPECT_FALSE(cfg.selectSubnet(selector)); selector.ciaddr_ = IOAddress("192.0.2.70"); EXPECT_FALSE(cfg.selectSubnet(selector)); selector.ciaddr_ = IOAddress("192.0.2.130"); EXPECT_FALSE(cfg.selectSubnet(selector)); // Finally, let's check that client without any classes is not supported. client_classes.clear(); selector.ciaddr_ = IOAddress("192.0.2.5"); EXPECT_FALSE(cfg.selectSubnet(selector)); selector.ciaddr_ = IOAddress("192.0.2.70"); EXPECT_FALSE(cfg.selectSubnet(selector)); selector.ciaddr_ = IOAddress("192.0.2.130"); EXPECT_FALSE(cfg.selectSubnet(selector)); } // This test verifies that shared network can be selected based on client // classification. TEST(CfgSubnets4Test, selectSharedNetworkByClasses) { IfaceMgrTestConfig config(true); CfgSubnets4 cfg; // Create 3 subnets. Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, SubnetID(1))); Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3, SubnetID(2))); Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3, SubnetID(3))); // Add them to the configuration. cfg.add(subnet1); cfg.add(subnet2); cfg.add(subnet3); // Create first network and add first two subnets to it. SharedNetwork4Ptr network1(new SharedNetwork4("network1")); network1->setIface("eth1"); network1->allowClientClass("device-type1"); ASSERT_NO_THROW(network1->add(subnet1)); ASSERT_NO_THROW(network1->add(subnet2)); // Create second network and add last subnet there. SharedNetwork4Ptr network2(new SharedNetwork4("network2")); network2->setIface("eth1"); network2->allowClientClass("device-type2"); ASSERT_NO_THROW(network2->add(subnet3)); // Use interface name as a selector. This guarantees that subnet // selection will be made based on the classification. SubnetSelector selector; selector.iface_name_ = "eth1"; // If the client has "device-type2" class, it is expected that the // second network will be used. This network has only one subnet // in it, i.e. subnet3. ClientClasses client_classes; client_classes.insert("device-type2"); selector.client_classes_ = client_classes; EXPECT_EQ(subnet3, cfg.selectSubnet(selector)); // Switch to device-type1 and expect that we assigned a subnet from // another shared network. client_classes.clear(); client_classes.insert("device-type1"); selector.client_classes_ = client_classes; Subnet4Ptr subnet = cfg.selectSubnet(selector); ASSERT_TRUE(subnet); SharedNetwork4Ptr network; subnet->getSharedNetwork(network); ASSERT_TRUE(network); EXPECT_EQ("network1", network->getName()); } // This test verifies the option selection can be used and is only // used when present. TEST(CfgSubnets4Test, selectSubnetByOptionSelect) { CfgSubnets4 cfg; // Create 3 subnets. Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, SubnetID(1))); Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3, SubnetID(2))); Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3, SubnetID(3))); // Add them to the configuration. cfg.add(subnet1); cfg.add(subnet2); cfg.add(subnet3); SubnetSelector selector; // Check that without option selection something else is used selector.ciaddr_ = IOAddress("192.0.2.5"); EXPECT_EQ(subnet1, cfg.selectSubnet(selector)); // The option selection has precedence selector.option_select_ = IOAddress("192.0.2.130"); EXPECT_EQ(subnet3, cfg.selectSubnet(selector)); // Over relay-info too selector.giaddr_ = IOAddress("10.0.0.1"); subnet2->addRelayAddress(IOAddress("10.0.0.1")); EXPECT_EQ(subnet3, cfg.selectSubnet(selector)); selector.option_select_ = IOAddress("0.0.0.0"); EXPECT_EQ(subnet2, cfg.selectSubnet(selector)); // Check that a not matching option selection it shall fail selector.option_select_ = IOAddress("10.0.0.1"); EXPECT_FALSE(cfg.selectSubnet(selector)); } // This test verifies that the relay information can be used to retrieve the // subnet. TEST(CfgSubnets4Test, selectSubnetByRelayAddress) { CfgSubnets4 cfg; // Create 3 subnets. Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, SubnetID(1))); Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3, SubnetID(2))); Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3, SubnetID(3))); // Add them to the configuration. cfg.add(subnet1); cfg.add(subnet2); cfg.add(subnet3); SubnetSelector selector; // Check that without relay-info specified, subnets are not selected selector.giaddr_ = IOAddress("10.0.0.1"); EXPECT_FALSE(cfg.selectSubnet(selector)); selector.giaddr_ = IOAddress("10.0.0.2"); EXPECT_FALSE(cfg.selectSubnet(selector)); selector.giaddr_ = IOAddress("10.0.0.3"); EXPECT_FALSE(cfg.selectSubnet(selector)); // Now specify relay info subnet1->addRelayAddress(IOAddress("10.0.0.1")); subnet2->addRelayAddress(IOAddress("10.0.0.2")); subnet3->addRelayAddress(IOAddress("10.0.0.3")); // And try again. This time relay-info is there and should match. selector.giaddr_ = IOAddress("10.0.0.1"); EXPECT_EQ(subnet1, cfg.selectSubnet(selector)); selector.giaddr_ = IOAddress("10.0.0.2"); EXPECT_EQ(subnet2, cfg.selectSubnet(selector)); selector.giaddr_ = IOAddress("10.0.0.3"); EXPECT_EQ(subnet3, cfg.selectSubnet(selector)); } // This test verifies that the relay information specified on the shared // network level can be used to select a subnet. TEST(CfgSubnets4Test, selectSharedNetworkByRelayAddressNetworkLevel) { CfgSubnets4 cfg; // Create 3 subnets. Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, SubnetID(1))); Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3, SubnetID(2))); Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3, SubnetID(3))); // Add them to the configuration. cfg.add(subnet1); cfg.add(subnet2); cfg.add(subnet3); SharedNetwork4Ptr network(new SharedNetwork4("network")); network->add(subnet2); SubnetSelector selector; // Now specify relay info. Note that for the second subnet we specify // relay info on the network level. subnet1->addRelayAddress(IOAddress("10.0.0.1")); network->addRelayAddress(IOAddress("10.0.0.2")); subnet3->addRelayAddress(IOAddress("10.0.0.3")); // And try again. This time relay-info is there and should match. selector.giaddr_ = IOAddress("10.0.0.1"); EXPECT_EQ(subnet1, cfg.selectSubnet(selector)); selector.giaddr_ = IOAddress("10.0.0.2"); EXPECT_EQ(subnet2, cfg.selectSubnet(selector)); selector.giaddr_ = IOAddress("10.0.0.3"); EXPECT_EQ(subnet3, cfg.selectSubnet(selector)); } // This test verifies that the relay information specified on the subnet // level can be used to select a subnet and the fact that a subnet belongs // to a shared network doesn't affect the process. TEST(CfgSubnets4Test, selectSharedNetworkByRelayAddressSubnetLevel) { CfgSubnets4 cfg; // Create 3 subnets. Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, SubnetID(1))); Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3, SubnetID(2))); Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3, SubnetID(3))); // Add them to the configuration. cfg.add(subnet1); cfg.add(subnet2); cfg.add(subnet3); SharedNetwork4Ptr network1(new SharedNetwork4("network1")); network1->add(subnet1); SharedNetwork4Ptr network2(new SharedNetwork4("network2")); network2->add(subnet2); SubnetSelector selector; // Now specify relay info. Note that for the second subnet we specify // relay info on the network level. subnet1->addRelayAddress(IOAddress("10.0.0.1")); subnet2->addRelayAddress(IOAddress("10.0.0.2")); subnet3->addRelayAddress(IOAddress("10.0.0.3")); // And try again. This time relay-info is there and should match. selector.giaddr_ = IOAddress("10.0.0.1"); EXPECT_EQ(subnet1, cfg.selectSubnet(selector)); selector.giaddr_ = IOAddress("10.0.0.2"); EXPECT_EQ(subnet2, cfg.selectSubnet(selector)); selector.giaddr_ = IOAddress("10.0.0.3"); EXPECT_EQ(subnet3, cfg.selectSubnet(selector)); } // This test verifies that the subnet can be selected for the client // using a source address if the client hasn't set the ciaddr. TEST(CfgSubnets4Test, selectSubnetNoCiaddr) { CfgSubnets4 cfg; // Create 3 subnets. Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, SubnetID(1))); Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3, SubnetID(2))); Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3, SubnetID(3))); // Make sure that initially the subnets don't exist. SubnetSelector selector; selector.remote_address_ = IOAddress("192.0.2.0"); // Set some unicast local address to simulate a Renew. selector.local_address_ = IOAddress("10.0.0.100"); ASSERT_FALSE(cfg.selectSubnet(selector)); // Add one subnet and make sure it is returned. cfg.add(subnet1); selector.remote_address_ = IOAddress("192.0.2.63"); EXPECT_EQ(subnet1, cfg.selectSubnet(selector)); // Add all other subnets. cfg.add(subnet2); cfg.add(subnet3); // Make sure they are returned for the appropriate addresses. selector.remote_address_ = IOAddress("192.0.2.15"); EXPECT_EQ(subnet1, cfg.selectSubnet(selector)); selector.remote_address_ = IOAddress("192.0.2.85"); EXPECT_EQ(subnet2, cfg.selectSubnet(selector)); selector.remote_address_ = IOAddress("192.0.2.191"); EXPECT_EQ(subnet3, cfg.selectSubnet(selector)); // Also, make sure that the NULL pointer is returned if the subnet // cannot be found. selector.remote_address_ = IOAddress("192.0.2.192"); EXPECT_FALSE(cfg.selectSubnet(selector)); } // This test verifies that the subnet can be selected using an address // set on the local interface. TEST(CfgSubnets4Test, selectSubnetInterface) { // The IfaceMgrTestConfig object initializes fake interfaces: // eth0, eth1 and lo on the configuration manager. The CfgSubnets4 // object uses addresses assigned to these fake interfaces to // select the appropriate subnet. IfaceMgrTestConfig config(true); CfgSubnets4 cfg; SubnetSelector selector; // Initially, there are no subnets configured, so none of the IPv4 // addresses assigned to eth0 and eth1 can match with any subnet. selector.iface_name_ = "eth0"; EXPECT_FALSE(cfg.selectSubnet(selector)); selector.iface_name_ = "eth1"; EXPECT_FALSE(cfg.selectSubnet(selector)); // Configure first subnet which address on eth0 corresponds to. Subnet4Ptr subnet1(new Subnet4(IOAddress("10.0.0.1"), 24, 1, 2, 3, SubnetID(1))); cfg.add(subnet1); // The address on eth0 should match the existing subnet. selector.iface_name_ = "eth0"; Subnet4Ptr subnet1_ret = cfg.selectSubnet(selector); ASSERT_TRUE(subnet1_ret); EXPECT_EQ(subnet1->get().first, subnet1_ret->get().first); // There should still be no match for eth1. selector.iface_name_ = "eth1"; EXPECT_FALSE(cfg.selectSubnet(selector)); // Configure a second subnet. Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.1"), 24, 1, 2, 3, SubnetID(2))); cfg.add(subnet2); // There should be match between eth0 and subnet1 and between eth1 and // subnet 2. selector.iface_name_ = "eth0"; subnet1_ret = cfg.selectSubnet(selector); ASSERT_TRUE(subnet1_ret); EXPECT_EQ(subnet1->get().first, subnet1_ret->get().first); selector.iface_name_ = "eth1"; Subnet4Ptr subnet2_ret = cfg.selectSubnet(selector); ASSERT_TRUE(subnet2_ret); EXPECT_EQ(subnet2->get().first, subnet2_ret->get().first); // This function throws an exception if the name of the interface is wrong. selector.iface_name_ = "bogus-interface"; EXPECT_THROW(cfg.selectSubnet(selector), isc::BadValue); } // Checks that detection of duplicated subnet IDs works as expected. It should // not be possible to add two IPv4 subnets holding the same ID. TEST(CfgSubnets4Test, duplication) { CfgSubnets4 cfg; Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, 123)); Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3, 124)); Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3, 123)); Subnet4Ptr subnet4(new Subnet4(IOAddress("192.0.2.1"), 26, 1, 2, 3, 125)); ASSERT_NO_THROW(cfg.add(subnet1)); EXPECT_NO_THROW(cfg.add(subnet2)); // Subnet 3 has the same ID as subnet 1. It shouldn't be able to add it. EXPECT_THROW(cfg.add(subnet3), isc::dhcp::DuplicateSubnetID); // Subnet 4 has a similar but different subnet as subnet 1. EXPECT_NO_THROW(cfg.add(subnet4)); } // This test checks if the IPv4 subnet can be selected based on the IPv6 address. TEST(CfgSubnets4Test, 4o6subnetMatchByAddress) { CfgSubnets4 cfg; Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, 123)); Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3, 124)); Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3, 125)); subnet2->get4o6().setSubnet4o6(IOAddress("2001:db8:1::"), 48); subnet3->get4o6().setSubnet4o6(IOAddress("2001:db8:2::"), 48); cfg.add(subnet1); cfg.add(subnet2); cfg.add(subnet3); SubnetSelector selector; selector.dhcp4o6_ = true; selector.remote_address_ = IOAddress("2001:db8:1::dead:beef"); EXPECT_EQ(subnet2, cfg.selectSubnet4o6(selector)); } // This test checks if the IPv4 subnet can be selected based on the value of // interface-id option. TEST(CfgSubnets4Test, 4o6subnetMatchByInterfaceId) { CfgSubnets4 cfg; Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, 123)); Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3, 124)); Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3, 125)); const uint8_t dummyPayload1[] = { 1, 2, 3, 4}; const uint8_t dummyPayload2[] = { 1, 2, 3, 5}; std::vector data1(dummyPayload1, dummyPayload1 + sizeof(dummyPayload1)); std::vector data2(dummyPayload2, dummyPayload2 + sizeof(dummyPayload2)); OptionPtr interfaceId1(new Option(Option::V6, D6O_INTERFACE_ID, data1)); OptionPtr interfaceId2(new Option(Option::V6, D6O_INTERFACE_ID, data2)); subnet2->get4o6().setInterfaceId(interfaceId1); cfg.add(subnet1); cfg.add(subnet2); cfg.add(subnet3); SubnetSelector selector; selector.dhcp4o6_ = true; selector.interface_id_ = interfaceId2; // We have mismatched interface-id options (data1 vs data2). Should not match. EXPECT_FALSE(cfg.selectSubnet4o6(selector)); // This time we have correct interface-id. Should match. selector.interface_id_ = interfaceId1; EXPECT_EQ(subnet2, cfg.selectSubnet4o6(selector)); } // This test checks if the IPv4 subnet can be selected based on the value of // interface name option. TEST(CfgSubnets4Test, 4o6subnetMatchByInterfaceName) { CfgSubnets4 cfg; Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, 123)); Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3, 124)); Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3, 125)); subnet2->get4o6().setIface4o6("eth7"); cfg.add(subnet1); cfg.add(subnet2); cfg.add(subnet3); SubnetSelector selector; selector.dhcp4o6_ = true; selector.iface_name_ = "eth5"; // We have mismatched interface names. Should not match. EXPECT_FALSE(cfg.selectSubnet4o6(selector)); // This time we have correct names. Should match. selector.iface_name_ = "eth7"; EXPECT_EQ(subnet2, cfg.selectSubnet4o6(selector)); } // This test check if IPv4 subnets can be unparsed in a predictable way, TEST(CfgSubnets4Test, unparseSubnet) { CfgSubnets4 cfg; // Add some subnets. Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, 123)); Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3, 124)); Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3, 125)); subnet1->allowClientClass("foo"); subnet1->setT1Percent(0.45); subnet1->setT2Percent(0.70); subnet1->setCacheThreshold(0.20); subnet2->setIface("lo"); subnet2->addRelayAddress(IOAddress("10.0.0.1")); subnet2->setValid(Triplet(100)); subnet2->setStoreExtendedInfo(true); subnet2->setCacheMaxAge(80); subnet2->setOfferLft(99); subnet3->setIface("eth1"); subnet3->requireClientClass("foo"); subnet3->requireClientClass("bar"); subnet3->setCalculateTeeTimes(true); subnet3->setT1Percent(0.50); subnet3->setT2Percent(0.65); subnet3->setReservationsGlobal(false); subnet3->setReservationsInSubnet(true); subnet3->setReservationsOutOfPool(false); subnet3->setAuthoritative(false); subnet3->setMatchClientId(true); subnet3->setSiaddr(IOAddress("192.0.2.2")); subnet3->setSname("frog"); subnet3->setFilename("/dev/null"); subnet3->setValid(Triplet(100, 200, 300)); subnet3->setDdnsSendUpdates(true); subnet3->setDdnsOverrideNoUpdate(true); subnet3->setDdnsOverrideClientUpdate(true); subnet3->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_ALWAYS); subnet3->setDdnsGeneratedPrefix("prefix"); subnet3->setDdnsQualifyingSuffix("example.com."); subnet3->setHostnameCharSet("[^A-Z]"); subnet3->setHostnameCharReplacement("x"); data::ElementPtr ctx1 = data::Element::fromJSON("{ \"comment\": \"foo\" }"); subnet1->setContext(ctx1); data::ElementPtr ctx2 = data::Element::createMap(); subnet2->setContext(ctx2); cfg.add(subnet1); cfg.add(subnet2); cfg.add(subnet3); // Unparse std::string expected = "[\n" "{\n" " \"id\": 123,\n" " \"subnet\": \"192.0.2.0/26\",\n" " \"t1-percent\": 0.45," " \"t2-percent\": 0.7," " \"cache-threshold\": .20,\n" " \"renew-timer\": 1,\n" " \"rebind-timer\": 2,\n" " \"relay\": { \"ip-addresses\": [ ] },\n" " \"valid-lifetime\": 3,\n" " \"min-valid-lifetime\": 3,\n" " \"max-valid-lifetime\": 3,\n" " \"client-class\": \"foo\",\n" " \"4o6-interface\": \"\",\n" " \"4o6-interface-id\": \"\",\n" " \"4o6-subnet\": \"\",\n" " \"option-data\": [ ],\n" " \"pools\": [ ],\n" " \"user-context\": { \"comment\": \"foo\" }\n" "},{\n" " \"id\": 124,\n" " \"subnet\": \"192.0.2.64/26\",\n" " \"interface\": \"lo\",\n" " \"renew-timer\": 1,\n" " \"rebind-timer\": 2,\n" " \"relay\": { \"ip-addresses\": [ \"10.0.0.1\" ] },\n" " \"valid-lifetime\": 100,\n" " \"min-valid-lifetime\": 100,\n" " \"max-valid-lifetime\": 100,\n" " \"4o6-interface\": \"\",\n" " \"4o6-interface-id\": \"\",\n" " \"4o6-subnet\": \"\",\n" " \"user-context\": {},\n" " \"option-data\": [ ],\n" " \"pools\": [ ],\n" " \"store-extended-info\": true,\n" " \"cache-max-age\": 80,\n" " \"offer-lifetime\": 99\n" "},{\n" " \"id\": 125,\n" " \"subnet\": \"192.0.2.128/26\",\n" " \"interface\": \"eth1\",\n" " \"match-client-id\": true,\n" " \"next-server\": \"192.0.2.2\",\n" " \"server-hostname\": \"frog\",\n" " \"boot-file-name\": \"/dev/null\",\n" " \"renew-timer\": 1,\n" " \"rebind-timer\": 2,\n" " \"relay\": { \"ip-addresses\": [ ] },\n" " \"valid-lifetime\": 200,\n" " \"min-valid-lifetime\": 100,\n" " \"max-valid-lifetime\": 300,\n" " \"4o6-interface\": \"\",\n" " \"4o6-interface-id\": \"\",\n" " \"4o6-subnet\": \"\",\n" " \"authoritative\": false,\n" " \"reservations-global\": false,\n" " \"reservations-in-subnet\": true,\n" " \"reservations-out-of-pool\": false,\n" " \"option-data\": [ ],\n" " \"pools\": [ ]\n," " \"require-client-classes\": [ \"foo\", \"bar\" ],\n" " \"calculate-tee-times\": true,\n" " \"t1-percent\": 0.50,\n" " \"t2-percent\": 0.65,\n" " \"ddns-generated-prefix\": \"prefix\",\n" " \"ddns-override-client-update\": true,\n" " \"ddns-override-no-update\": true,\n" " \"ddns-qualifying-suffix\": \"example.com.\",\n" " \"ddns-replace-client-name\": \"always\",\n" " \"ddns-send-updates\": true,\n" " \"hostname-char-replacement\": \"x\",\n" " \"hostname-char-set\": \"[^A-Z]\"\n" "} ]\n"; runToElementTest(expected, cfg); } // This test check if IPv4 pools can be unparsed in a predictable way, TEST(CfgSubnets4Test, unparsePool) { CfgSubnets4 cfg; // Add a subnet with pools Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.2.0"), 24, 1, 2, 3, 123)); Pool4Ptr pool1(new Pool4(IOAddress("192.0.2.1"), IOAddress("192.0.2.10"))); Pool4Ptr pool2(new Pool4(IOAddress("192.0.2.64"), 26)); pool2->allowClientClass("bar"); std::string json1 = "{ \"comment\": \"foo\", \"version\": 1 }"; data::ElementPtr ctx1 = data::Element::fromJSON(json1); pool1->setContext(ctx1); data::ElementPtr ctx2 = data::Element::fromJSON("{ \"foo\": \"bar\" }"); pool2->setContext(ctx2); pool2->requireClientClass("foo"); subnet->addPool(pool1); subnet->addPool(pool2); cfg.add(subnet); // Unparse std::string expected = "[\n" "{\n" " \"id\": 123,\n" " \"subnet\": \"192.0.2.0/24\",\n" " \"renew-timer\": 1,\n" " \"rebind-timer\": 2,\n" " \"relay\": { \"ip-addresses\": [ ] },\n" " \"valid-lifetime\": 3,\n" " \"min-valid-lifetime\": 3,\n" " \"max-valid-lifetime\": 3,\n" " \"4o6-interface\": \"\",\n" " \"4o6-interface-id\": \"\",\n" " \"4o6-subnet\": \"\",\n" " \"option-data\": [],\n" " \"pools\": [\n" " {\n" " \"option-data\": [ ],\n" " \"pool\": \"192.0.2.1-192.0.2.10\",\n" " \"user-context\": { \"comment\": \"foo\",\n" " \"version\": 1 }\n" " },{\n" " \"option-data\": [ ],\n" " \"pool\": \"192.0.2.64/26\",\n" " \"user-context\": { \"foo\": \"bar\" },\n" " \"client-class\": \"bar\",\n" " \"require-client-classes\": [ \"foo\" ]\n" " }\n" " ]\n" "} ]\n"; runToElementTest(expected, cfg); } // This test verifies that it is possible to retrieve a subnet using subnet-id. TEST(CfgSubnets4Test, getSubnet) { CfgSubnets4 cfg; // Create 3 subnets. Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, 100)); Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3, 200)); Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3, 300)); // Add one subnet and make sure it is returned. cfg.add(subnet1); cfg.add(subnet2); cfg.add(subnet3); EXPECT_EQ(subnet1, cfg.getSubnet(100)); EXPECT_EQ(subnet2, cfg.getSubnet(200)); EXPECT_EQ(subnet3, cfg.getSubnet(300)); EXPECT_EQ(Subnet4Ptr(), cfg.getSubnet(400)); // no such subnet } // This test verifies that hasSubnetWithServerId returns correct value. TEST(CfgSubnets4Test, hasSubnetWithServerId) { CfgSubnets4 cfg; // Initially, there is no server identifier option present. EXPECT_FALSE(cfg.hasSubnetWithServerId(IOAddress("1.2.3.4"))); OptionDefinitionPtr def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE, DHO_DHCP_SERVER_IDENTIFIER); OptionCustomPtr opt_server_id(new OptionCustom(*def, Option::V4)); opt_server_id->writeAddress(IOAddress("1.2.3.4")); Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, 100)); subnet->getCfgOption()->add(opt_server_id, false, false, DHCP4_OPTION_SPACE); cfg.add(subnet); EXPECT_TRUE(cfg.hasSubnetWithServerId(IOAddress("1.2.3.4"))); EXPECT_FALSE(cfg.hasSubnetWithServerId(IOAddress("2.3.4.5"))); } // This test verifies the Subnet4 parser's validation logic for // t1-percent and t2-percent parameters. TEST(CfgSubnets4Test, teeTimePercentValidation) { // Describes a single test scenario. struct Scenario { std::string label; // label used for logging test failures bool calculate_tee_times; // value of calculate-tee-times parameter double t1_percent; // value of t1-percent parameter double t2_percent; // value of t2-percent parameter std::string error_message; // expected error message is parsing should fail }; // Test Scenarios. std::vector tests = { {"off and valid", false, .5, .95, ""}, {"on and valid", true, .5, .95, ""}, {"t2_negative", true, .5, -.95, "subnet configuration failed: t2-percent:" " -0.95 is invalid, it must be greater than 0.0 and less than 1.0" }, {"t2_too_big", true, .5, 1.95, "subnet configuration failed: t2-percent:" " 1.95 is invalid, it must be greater than 0.0 and less than 1.0" }, {"t1_negative", true, -.5, .95, "subnet configuration failed: t1-percent:" " -0.5 is invalid it must be greater than 0.0 and less than 1.0" }, {"t1_too_big", true, 1.5, .95, "subnet configuration failed: t1-percent:" " 1.5 is invalid it must be greater than 0.0 and less than 1.0" }, {"t1_bigger_than_t2", true, .85, .45, "subnet configuration failed: t1-percent:" " 0.85 is invalid, it must be less than t2-percent: 0.45" } }; // First we create a set of elements that provides all // required for a Subnet4. std::string json = " {" " \"id\": 1,\n" " \"subnet\": \"10.1.2.0/24\", \n" " \"interface\": \"\", \n" " \"renew-timer\": 100, \n" " \"rebind-timer\": 200, \n" " \"valid-lifetime\": 300, \n" " \"match-client-id\": false, \n" " \"authoritative\": false, \n" " \"next-server\": \"\", \n" " \"server-hostname\": \"\", \n" " \"boot-file-name\": \"\", \n" " \"client-class\": \"\", \n" " \"require-client-classes\": [] \n," " \"reservations-global\": false, \n" " \"reservations-in-subnet\": true, \n" " \"reservations-out-of-pool\": false, \n" " \"4o6-interface\": \"\", \n" " \"4o6-interface-id\": \"\", \n" " \"4o6-subnet\": \"\" \n" " }"; data::ElementPtr elems; ASSERT_NO_THROW(elems = data::Element::fromJSON(json)) << "invalid JSON:" << json << "\n test is broken"; // Iterate over the test scenarios, verifying each prescribed // outcome. for (auto test = tests.begin(); test != tests.end(); ++test) { { SCOPED_TRACE("test: " + (*test).label); // Set this scenario's configuration parameters elems->set("calculate-tee-times", data::Element::create((*test).calculate_tee_times)); elems->set("t1-percent", data::Element::create((*test).t1_percent)); elems->set("t2-percent", data::Element::create((*test).t2_percent)); Subnet4Ptr subnet; try { // Attempt to parse the configuration. Subnet4ConfigParser parser; subnet = parser.parse(elems); } catch (const std::exception& ex) { if (!(*test).error_message.empty()) { // We expected a failure, did we fail the correct way? EXPECT_EQ((*test).error_message, ex.what()); } else { // Should not have failed. ADD_FAILURE() << "Scenario should not have failed: " << ex.what(); } // Either way we're done with this scenario. continue; } // We parsed correctly, make sure the values are right. EXPECT_EQ((*test).calculate_tee_times, subnet->getCalculateTeeTimes()); EXPECT_TRUE(util::areDoublesEquivalent((*test).t1_percent, subnet->getT1Percent())); EXPECT_TRUE(util::areDoublesEquivalent((*test).t2_percent, subnet->getT2Percent())); } } } // This test verifies the Subnet4 parser's validation logic for // valid-lifetime and indirectly shared lifetime parsing. TEST(CfgSubnets4Test, validLifetimeValidation) { // First we create a set of elements that provides all // required for a Subnet4. std::string json = " {" " \"id\": 1,\n" " \"subnet\": \"10.1.2.0/24\", \n" " \"interface\": \"\", \n" " \"renew-timer\": 100, \n" " \"rebind-timer\": 200, \n" " \"match-client-id\": false, \n" " \"authoritative\": false, \n" " \"next-server\": \"\", \n" " \"server-hostname\": \"\", \n" " \"boot-file-name\": \"\", \n" " \"client-class\": \"\", \n" " \"require-client-classes\": [] \n," " \"reservations-global\": false, \n" " \"reservations-in-subnet\": true, \n" " \"reservations-out-of-pool\": false, \n" " \"4o6-interface\": \"\", \n" " \"4o6-interface-id\": \"\", \n" " \"4o6-subnet\": \"\" \n" " }"; data::ElementPtr elems; ASSERT_NO_THROW(elems = data::Element::fromJSON(json)) << "invalid JSON:" << json << "\n test is broken"; { SCOPED_TRACE("no valid-lifetime"); data::ElementPtr copied = data::copy(elems); Subnet4Ptr subnet; Subnet4ConfigParser parser; ASSERT_NO_THROW(subnet = parser.parse(copied)); ASSERT_TRUE(subnet); EXPECT_TRUE(subnet->getValid().unspecified()); data::ConstElementPtr repr = subnet->toElement(); EXPECT_FALSE(repr->get("valid-lifetime")); EXPECT_FALSE(repr->get("min-valid-lifetime")); EXPECT_FALSE(repr->get("max-valid-lifetime")); } { SCOPED_TRACE("valid-lifetime only"); data::ElementPtr copied = data::copy(elems); copied->set("valid-lifetime", data::Element::create(100)); Subnet4Ptr subnet; Subnet4ConfigParser parser; ASSERT_NO_THROW(subnet = parser.parse(copied)); ASSERT_TRUE(subnet); EXPECT_FALSE(subnet->getValid().unspecified()); EXPECT_EQ(100, subnet->getValid().get()); EXPECT_EQ(100, subnet->getValid().getMin()); EXPECT_EQ(100, subnet->getValid().getMax()); data::ConstElementPtr repr = subnet->toElement(); data::ConstElementPtr value = repr->get("valid-lifetime"); ASSERT_TRUE(value); EXPECT_EQ("100", value->str()); data::ConstElementPtr min_value = repr->get("min-valid-lifetime"); ASSERT_TRUE(min_value); EXPECT_EQ("100", min_value->str()); data::ConstElementPtr max_value = repr->get("max-valid-lifetime"); ASSERT_TRUE(max_value); EXPECT_EQ("100", max_value->str()); } { SCOPED_TRACE("min-valid-lifetime only"); data::ElementPtr copied = data::copy(elems); copied->set("min-valid-lifetime", data::Element::create(100)); Subnet4Ptr subnet; Subnet4ConfigParser parser; ASSERT_NO_THROW(subnet = parser.parse(copied)); ASSERT_TRUE(subnet); EXPECT_FALSE(subnet->getValid().unspecified()); EXPECT_EQ(100, subnet->getValid().get()); EXPECT_EQ(100, subnet->getValid().getMin()); EXPECT_EQ(100, subnet->getValid().getMax()); data::ConstElementPtr repr = subnet->toElement(); data::ConstElementPtr value = repr->get("valid-lifetime"); ASSERT_TRUE(value); EXPECT_EQ("100", value->str()); data::ConstElementPtr min_value = repr->get("min-valid-lifetime"); ASSERT_TRUE(min_value); EXPECT_EQ("100", min_value->str()); data::ConstElementPtr max_value = repr->get("max-valid-lifetime"); ASSERT_TRUE(max_value); EXPECT_EQ("100", max_value->str()); } { SCOPED_TRACE("max-valid-lifetime only"); data::ElementPtr copied = data::copy(elems); copied->set("max-valid-lifetime", data::Element::create(100)); Subnet4Ptr subnet; Subnet4ConfigParser parser; ASSERT_NO_THROW(subnet = parser.parse(copied)); ASSERT_TRUE(subnet); EXPECT_FALSE(subnet->getValid().unspecified()); EXPECT_EQ(100, subnet->getValid().get()); EXPECT_EQ(100, subnet->getValid().getMin()); EXPECT_EQ(100, subnet->getValid().getMax()); data::ConstElementPtr repr = subnet->toElement(); data::ConstElementPtr value = repr->get("valid-lifetime"); ASSERT_TRUE(value); EXPECT_EQ("100", value->str()); data::ConstElementPtr min_value = repr->get("min-valid-lifetime"); ASSERT_TRUE(min_value); EXPECT_EQ("100", min_value->str()); data::ConstElementPtr max_value = repr->get("max-valid-lifetime"); ASSERT_TRUE(max_value); EXPECT_EQ("100", max_value->str()); } { SCOPED_TRACE("min-valid-lifetime and valid-lifetime"); data::ElementPtr copied = data::copy(elems); copied->set("min-valid-lifetime", data::Element::create(100)); // Use a different (and greater) value for the default. copied->set("valid-lifetime", data::Element::create(200)); Subnet4Ptr subnet; Subnet4ConfigParser parser; ASSERT_NO_THROW(subnet = parser.parse(copied)); ASSERT_TRUE(subnet); EXPECT_FALSE(subnet->getValid().unspecified()); EXPECT_EQ(200, subnet->getValid().get()); EXPECT_EQ(100, subnet->getValid().getMin()); EXPECT_EQ(200, subnet->getValid().getMax()); data::ConstElementPtr repr = subnet->toElement(); data::ConstElementPtr value = repr->get("valid-lifetime"); ASSERT_TRUE(value); EXPECT_EQ("200", value->str()); data::ConstElementPtr min_value = repr->get("min-valid-lifetime"); ASSERT_TRUE(min_value); EXPECT_EQ("100", min_value->str()); data::ConstElementPtr max_value = repr->get("max-valid-lifetime"); ASSERT_TRUE(max_value); EXPECT_EQ("200", max_value->str()); } { SCOPED_TRACE("max-valid-lifetime and valid-lifetime"); data::ElementPtr copied = data::copy(elems); copied->set("max-valid-lifetime", data::Element::create(200)); // Use a different (and smaller) value for the default. copied->set("valid-lifetime", data::Element::create(100)); Subnet4Ptr subnet; Subnet4ConfigParser parser; ASSERT_NO_THROW(subnet = parser.parse(copied)); ASSERT_TRUE(subnet); EXPECT_FALSE(subnet->getValid().unspecified()); EXPECT_EQ(100, subnet->getValid().get()); EXPECT_EQ(100, subnet->getValid().getMin()); EXPECT_EQ(200, subnet->getValid().getMax()); data::ConstElementPtr repr = subnet->toElement(); data::ConstElementPtr value = repr->get("valid-lifetime"); ASSERT_TRUE(value); EXPECT_EQ("100", value->str()); data::ConstElementPtr min_value = repr->get("min-valid-lifetime"); ASSERT_TRUE(min_value); EXPECT_EQ("100", min_value->str()); data::ConstElementPtr max_value = repr->get("max-valid-lifetime"); ASSERT_TRUE(max_value); EXPECT_EQ("200", max_value->str()); } { SCOPED_TRACE("min-valid-lifetime and max-valid-lifetime"); data::ElementPtr copied = data::copy(elems); copied->set("min-valid-lifetime", data::Element::create(100)); copied->set("max-valid-lifetime", data::Element::create(200)); Subnet4ConfigParser parser; // No idea about the value to use for the default so failing. ASSERT_THROW(parser.parse(copied), DhcpConfigError); } { SCOPED_TRACE("all 3 (min, max and default) valid-lifetime"); data::ElementPtr copied = data::copy(elems); copied->set("min-valid-lifetime", data::Element::create(100)); // Use a different (and greater) value for the default. copied->set("valid-lifetime", data::Element::create(200)); // Use a different (and greater than both) value for max. copied->set("max-valid-lifetime", data::Element::create(300)); Subnet4Ptr subnet; Subnet4ConfigParser parser; ASSERT_NO_THROW(subnet = parser.parse(copied)); ASSERT_TRUE(subnet); EXPECT_FALSE(subnet->getValid().unspecified()); EXPECT_EQ(200, subnet->getValid().get()); EXPECT_EQ(100, subnet->getValid().getMin()); EXPECT_EQ(300, subnet->getValid().getMax()); data::ConstElementPtr repr = subnet->toElement(); data::ConstElementPtr value = repr->get("valid-lifetime"); ASSERT_TRUE(value); EXPECT_EQ("200", value->str()); data::ConstElementPtr min_value = repr->get("min-valid-lifetime"); ASSERT_TRUE(min_value); EXPECT_EQ("100", min_value->str()); data::ConstElementPtr max_value = repr->get("max-valid-lifetime"); ASSERT_TRUE(max_value); EXPECT_EQ("300", max_value->str()); } { SCOPED_TRACE("default value too small"); data::ElementPtr copied = data::copy(elems); copied->set("min-valid-lifetime", data::Element::create(200)); // 100 < 200 so it will fail. copied->set("valid-lifetime", data::Element::create(100)); // Use a different (and greater than both) value for max. copied->set("max-valid-lifetime", data::Element::create(300)); Subnet4ConfigParser parser; ASSERT_THROW(parser.parse(copied), DhcpConfigError); } { SCOPED_TRACE("default value too large"); data::ElementPtr copied = data::copy(elems); copied->set("min-valid-lifetime", data::Element::create(100)); // Use a different (and greater) value for the default. copied->set("valid-lifetime", data::Element::create(300)); // 300 > 200 so it will fail. copied->set("max-valid-lifetime", data::Element::create(200)); Subnet4ConfigParser parser; ASSERT_THROW(parser.parse(copied), DhcpConfigError); } { SCOPED_TRACE("equal bounds are no longer ignored"); data::ElementPtr copied = data::copy(elems); copied->set("min-valid-lifetime", data::Element::create(100)); copied->set("valid-lifetime", data::Element::create(100)); copied->set("max-valid-lifetime", data::Element::create(100)); Subnet4Ptr subnet; Subnet4ConfigParser parser; ASSERT_NO_THROW(subnet = parser.parse(copied)); ASSERT_TRUE(subnet); EXPECT_FALSE(subnet->getValid().unspecified()); EXPECT_EQ(100, subnet->getValid().get()); EXPECT_EQ(100, subnet->getValid().getMin()); EXPECT_EQ(100, subnet->getValid().getMax()); data::ConstElementPtr repr = subnet->toElement(); data::ConstElementPtr value = repr->get("valid-lifetime"); ASSERT_TRUE(value); EXPECT_EQ("100", value->str()); data::ConstElementPtr min_value = repr->get("min-valid-lifetime"); ASSERT_TRUE(min_value); EXPECT_EQ("100", min_value->str()); data::ConstElementPtr max_value = repr->get("max-valid-lifetime"); ASSERT_TRUE(max_value); EXPECT_EQ("100", max_value->str()); } } // This test verifies the Subnet4 parser's validation logic for // hostname sanitizer values. TEST(CfgSubnets4Test, hostnameSanitizierValidation) { // First we create a set of elements that provides all // required for a Subnet4. std::string json = " {" " \"id\": 1,\n" " \"subnet\": \"10.1.2.0/24\", \n" " \"interface\": \"\", \n" " \"renew-timer\": 100, \n" " \"rebind-timer\": 200, \n" " \"match-client-id\": false, \n" " \"authoritative\": false, \n" " \"next-server\": \"\", \n" " \"server-hostname\": \"\", \n" " \"boot-file-name\": \"\", \n" " \"client-class\": \"\", \n" " \"require-client-classes\": [] \n," " \"reservations-global\": false, \n" " \"reservations-in-subnet\": true, \n" " \"reservations-out-of-pool\": false, \n" " \"4o6-interface\": \"\", \n" " \"4o6-interface-id\": \"\", \n" " \"4o6-subnet\": \"\" \n" " }"; data::ElementPtr elems; ASSERT_NO_THROW(elems = data::Element::fromJSON(json)) << "invalid JSON:" << json << "\n test is broken"; { SCOPED_TRACE("invalid regular expression"); data::ElementPtr copied = data::copy(elems); copied->set("hostname-char-set", data::Element::create("^[A-")); copied->set("hostname-char-replacement", data::Element::create("x")); Subnet4Ptr subnet; Subnet4ConfigParser parser; EXPECT_THROW_MSG(subnet = parser.parse(copied), DhcpConfigError, "subnet configuration failed: hostname-char-set " "'^[A-' is not a valid regular expression"); } { SCOPED_TRACE("valid regular expression"); data::ElementPtr copied = data::copy(elems); copied->set("hostname-char-set", data::Element::create("^[A-Z]")); copied->set("hostname-char-replacement", data::Element::create("x")); Subnet4Ptr subnet; Subnet4ConfigParser parser; ASSERT_NO_THROW(subnet = parser.parse(copied)); EXPECT_EQ("^[A-Z]", subnet->getHostnameCharSet().get()); EXPECT_EQ("x", subnet->getHostnameCharReplacement().get()); } } // This test verifies the Subnet4 parser's validation logic for // lease cache parameters. TEST(CfgSubnets4Test, cacheParamValidation) { // Describes a single test scenario. struct Scenario { std::string label; // label used for logging test failures double threshold; // value of cache-threshold std::string error_message; // expected error message is parsing should fail }; // Test Scenarios. std::vector tests = { {"valid", .25, ""}, {"negative", -.25, "subnet configuration failed: cache-threshold:" " -0.25 is invalid, it must be greater than 0.0 and less than 1.0" }, {"too big", 1.05, "subnet configuration failed: cache-threshold:" " 1.05 is invalid, it must be greater than 0.0 and less than 1.0" } }; // First we create a set of elements that provides all // required for a Subnet4. std::string json = " {" " \"id\": 1,\n" " \"subnet\": \"10.1.2.0/24\", \n" " \"interface\": \"\", \n" " \"renew-timer\": 100, \n" " \"rebind-timer\": 200, \n" " \"valid-lifetime\": 300, \n" " \"match-client-id\": false, \n" " \"authoritative\": false, \n" " \"next-server\": \"\", \n" " \"server-hostname\": \"\", \n" " \"boot-file-name\": \"\", \n" " \"client-class\": \"\", \n" " \"require-client-classes\": [] \n," " \"reservations-global\": false, \n" " \"reservations-in-subnet\": true, \n" " \"reservations-out-of-pool\": false, \n" " \"4o6-interface\": \"\", \n" " \"4o6-interface-id\": \"\", \n" " \"4o6-subnet\": \"\" \n" " }"; data::ElementPtr elems; ASSERT_NO_THROW(elems = data::Element::fromJSON(json)) << "invalid JSON:" << json << "\n test is broken"; // Iterate over the test scenarios, verifying each prescribed // outcome. for (auto test = tests.begin(); test != tests.end(); ++test) { { SCOPED_TRACE("test: " + (*test).label); // Set this scenario's configuration parameters elems->set("cache-threshold", data::Element::create((*test).threshold)); Subnet4Ptr subnet; try { // Attempt to parse the configuration. Subnet4ConfigParser parser; subnet = parser.parse(elems); } catch (const std::exception& ex) { if (!(*test).error_message.empty()) { // We expected a failure, did we fail the correct way? EXPECT_EQ((*test).error_message, ex.what()); } else { // Should not have failed. ADD_FAILURE() << "Scenario should not have failed: " << ex.what(); } // Either way we're done with this scenario. continue; } // We parsed correctly, make sure the values are right. EXPECT_TRUE(util::areDoublesEquivalent((*test).threshold, subnet->getCacheThreshold())); } } } // This test verifies that the optional interface check works as expected. TEST(CfgSubnets4Test, iface) { // Create a configuration. std::string json = " {" " \"id\": 1,\n" " \"subnet\": \"10.1.2.0/24\", \n" " \"interface\": \"eth1961\"\n" " }"; data::ElementPtr elems; ASSERT_NO_THROW(elems = data::Element::fromJSON(json)) << "invalid JSON:" << json << "\n test is broken"; // The interface check can be disabled. Subnet4ConfigParser parser_no_check(false); Subnet4Ptr subnet; EXPECT_NO_THROW(subnet = parser_no_check.parse(elems)); ASSERT_TRUE(subnet); EXPECT_FALSE(subnet->getIface().unspecified()); EXPECT_EQ("eth1961", subnet->getIface().get()); // Retry with the interface check enabled. Subnet4ConfigParser parser; EXPECT_THROW(parser.parse(elems), DhcpConfigError); // Configure default test interfaces. IfaceMgrTestConfig config(true); EXPECT_NO_THROW(subnet = parser_no_check.parse(elems)); ASSERT_TRUE(subnet); EXPECT_FALSE(subnet->getIface().unspecified()); EXPECT_EQ("eth1961", subnet->getIface().get()); EXPECT_NO_THROW(subnet = parser.parse(elems)); ASSERT_TRUE(subnet); EXPECT_FALSE(subnet->getIface().unspecified()); EXPECT_EQ("eth1961", subnet->getIface().get()); } // This test verifies that update statistics works as expected. TEST(CfgSubnets4Test, updateStatistics) { CfgMgr::instance().clear(); CfgSubnets4Ptr cfg = CfgMgr::instance().getCurrentCfg()->getCfgSubnets4(); ObservationPtr observation; SubnetID subnet_id = 100; LeaseMgrFactory::create("type=memfile universe=4 persist=false"); // remove all statistics StatsMgr::instance().removeAll(); // Create subnet. Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, 100)); Pool4Ptr pool(new Pool4(IOAddress("192.0.2.0"), 26)); subnet->addPool(pool); // Add subnet. cfg->add(subnet); observation = StatsMgr::instance().getObservation( "cumulative-assigned-addresses"); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( "declined-addresses"); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( "reclaimed-declined-addresses"); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( "reclaimed-leases"); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "total-addresses")); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "total-addresses"))); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "assigned-addresses")); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "assigned-addresses"))); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "cumulative-assigned-addresses")); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "cumulative-assigned-addresses"))); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "declined-addresses")); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "declined-addresses"))); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "reclaimed-declined-addresses")); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "reclaimed-declined-addresses"))); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "reclaimed-leases")); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "reclaimed-leases"))); ASSERT_FALSE(observation); cfg->updateStatistics(); observation = StatsMgr::instance().getObservation( "cumulative-assigned-addresses"); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); observation = StatsMgr::instance().getObservation( "declined-addresses"); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); observation = StatsMgr::instance().getObservation( "reclaimed-declined-addresses"); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); observation = StatsMgr::instance().getObservation( "reclaimed-leases"); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "total-addresses")); ASSERT_TRUE(observation); ASSERT_EQ(64, observation->getInteger().first); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "total-addresses"))); ASSERT_TRUE(observation); ASSERT_EQ(64, observation->getInteger().first); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "assigned-addresses")); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "assigned-addresses"))); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "cumulative-assigned-addresses")); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "cumulative-assigned-addresses"))); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "declined-addresses")); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "declined-addresses"))); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "reclaimed-declined-addresses")); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "reclaimed-declined-addresses"))); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "reclaimed-leases")); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "reclaimed-leases"))); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); } // This test verifies that remove statistics works as expected. TEST(CfgSubnets4Test, removeStatistics) { CfgSubnets4 cfg; ObservationPtr observation; SubnetID subnet_id = 100; // remove all statistics and then set them all to 0 StatsMgr::instance().removeAll(); // Create subnet. Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, 100)); Pool4Ptr pool(new Pool4(IOAddress("192.0.2.0"), 26)); subnet->addPool(pool); // Add subnet. cfg.add(subnet); StatsMgr::instance().setValue( StatsMgr::generateName("subnet", subnet_id, "total-addresses"), int64_t(0)); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "total-addresses")); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); StatsMgr::instance().setValue( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "total-addresses")), int64_t(0)); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "total-addresses"))); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); StatsMgr::instance().setValue( StatsMgr::generateName("subnet", subnet_id, "assigned-addresses"), int64_t(0)); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "assigned-addresses")); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); StatsMgr::instance().setValue( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "assigned-addresses")), int64_t(0)); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "assigned-addresses"))); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); StatsMgr::instance().setValue( StatsMgr::generateName("subnet", subnet_id, "cumulative-assigned-addresses"), int64_t(0)); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "cumulative-assigned-addresses")); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); StatsMgr::instance().setValue( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "cumulative-assigned-addresses")), int64_t(0)); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "cumulative-assigned-addresses"))); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); StatsMgr::instance().setValue( StatsMgr::generateName("subnet", subnet_id, "declined-addresses"), int64_t(0)); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "declined-addresses")); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); StatsMgr::instance().setValue( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "declined-addresses")), int64_t(0)); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "declined-addresses"))); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); StatsMgr::instance().setValue( StatsMgr::generateName("subnet", subnet_id, "reclaimed-declined-addresses"), int64_t(0)); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "reclaimed-declined-addresses")); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); StatsMgr::instance().setValue( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "reclaimed-declined-addresses")), int64_t(0)); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "reclaimed-declined-addresses"))); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); StatsMgr::instance().setValue( StatsMgr::generateName("subnet", subnet_id, "reclaimed-leases"), int64_t(0)); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "reclaimed-leases")); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); StatsMgr::instance().setValue( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "reclaimed-leases")), int64_t(0)); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "reclaimed-leases"))); ASSERT_TRUE(observation); ASSERT_EQ(0, observation->getInteger().first); // remove all statistics cfg.removeStatistics(); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "total-addresses")); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "total-addresses"))); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "assigned-addresses")); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "assigned-addresses"))); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "cumulative-assigned-addresses")); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "cumulative-assigned-addresses"))); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "declined-addresses")); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "declined-addresses"))); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "reclaimed-declined-addresses")); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "reclaimed-declined-addresses"))); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, "reclaimed-leases")); ASSERT_FALSE(observation); observation = StatsMgr::instance().getObservation( StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, "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 within "; msg += "the IPv4 subnet '10.1.2.0/24'"; EXPECT_THROW_MSG(parser.parse(elems), DhcpConfigError, msg); } // This test verifies that the getLinks tool works as expected. TEST(CfgSubnets4Test, getLinks) { CfgSubnets4 cfg; // Create 4 subnets. Subnet4Ptr subnet0(new Subnet4(IOAddress("0.0.0.0"), 24, 1, 2, 3, SubnetID(111))); Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.1.0"), 26, 1, 2, 3, SubnetID(1))); Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, SubnetID(2))); Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.3.0"), 26, 1, 2, 3, SubnetID(3))); // Add all subnets to the configuration. ASSERT_NO_THROW(cfg.add(subnet0)); ASSERT_NO_THROW(cfg.add(subnet1)); ASSERT_NO_THROW(cfg.add(subnet2)); ASSERT_NO_THROW(cfg.add(subnet3)); // No 192.0.4.0 subnet. SubnetIDSet links; uint8_t link_len = 111; EXPECT_NO_THROW(links = cfg.getLinks(IOAddress("192.0.4.0"), link_len)); EXPECT_TRUE(links.empty()); EXPECT_EQ(111, link_len); // A 192.0.2.0/26 subnet. links.clear(); link_len = 111; EXPECT_NO_THROW(links = cfg.getLinks(IOAddress("192.0.2.0"), link_len)); SubnetIDSet expected = { 2 }; EXPECT_EQ(expected, links); EXPECT_EQ(26, link_len); // Check that any address in the subnet works. links.clear(); link_len = 111; EXPECT_NO_THROW(links = cfg.getLinks(IOAddress("192.0.2.23"), link_len)); EXPECT_EQ(expected, links); EXPECT_EQ(26, link_len); // Check that an address outside the subnet does not work. links.clear(); link_len = 111; EXPECT_NO_THROW(links = cfg.getLinks(IOAddress("192.0.2.123"), link_len)); EXPECT_TRUE(links.empty()); EXPECT_EQ(111, link_len); // Add a second 192.0.2.0/26 subnet. Subnet4Ptr subnet10(new Subnet4(IOAddress("192.0.2.10"), 26, 1, 2, 3, SubnetID(10))); ASSERT_NO_THROW(cfg.add(subnet10)); // Now we should get 2 subnets. links.clear(); link_len = 111; EXPECT_NO_THROW(links = cfg.getLinks(IOAddress("192.0.2.0"), link_len)); expected = { 2, 10 }; EXPECT_EQ(expected, links); EXPECT_EQ(26, link_len); // Add a larger subnet. Subnet4Ptr subnet20(new Subnet4(IOAddress("192.0.2.20"), 24, 1, 2, 3, SubnetID(20))); ASSERT_NO_THROW(cfg.add(subnet20)); // Now we should get 3 subnets and a smaller prefix length. links.clear(); link_len = 111; EXPECT_NO_THROW(links = cfg.getLinks(IOAddress("192.0.2.0"), link_len)); expected = { 2, 10, 20 }; EXPECT_EQ(expected, links); EXPECT_EQ(24, link_len); // But only the larger subnet if the address is only in it. links.clear(); link_len = 111; EXPECT_NO_THROW(links = cfg.getLinks(IOAddress("192.0.2.123"), link_len)); expected = { 20 }; EXPECT_EQ(expected, links); EXPECT_EQ(24, link_len); // Even it is not used for Bulk Leasequery, it works for 0.0.0.0 too. links.clear(); link_len = 111; EXPECT_NO_THROW(links = cfg.getLinks(IOAddress("0.0.0.0"), link_len)); expected = { 111 }; EXPECT_EQ(expected, links); EXPECT_EQ(24, link_len); } // This test verifies that for each subnet in the configuration it calls // the initAllocatorAfterConfigure function. TEST(CfgSubnets4Test, initAllocatorsAfterConfigure) { CfgSubnets4 cfg; // Create 4 subnets. Subnet4Ptr subnet0(new Subnet4(IOAddress("0.0.0.0"), 24, 1, 2, 3, SubnetID(111))); Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.1.0"), 26, 1, 2, 3, SubnetID(1))); Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, SubnetID(2))); Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.3.0"), 26, 1, 2, 3, SubnetID(3))); auto allocator0 = boost::make_shared(Lease::TYPE_V4, subnet0); subnet0->setAllocator(Lease::TYPE_V4, allocator0); auto allocator2 = boost::make_shared(Lease::TYPE_V4, subnet2); subnet2->setAllocator(Lease::TYPE_V4, allocator2); cfg.add(subnet0); cfg.add(subnet1); cfg.add(subnet2); cfg.add(subnet3); EXPECT_EQ(0, allocator0->callcount_); EXPECT_EQ(0, allocator2->callcount_); cfg.initAllocatorsAfterConfigure(); EXPECT_EQ(1, allocator0->callcount_); EXPECT_EQ(1, allocator2->callcount_); } /// @brief Test fixture for parsing v4 Subnets that can verify log output. class Subnet4ParserTest : public LogContentTest { public: /// @brief virtual destructor virtual ~Subnet4ParserTest() = default; }; // This test verifies that subnet parser for IPv4 works properly // when using invalid renew and rebind timers. TEST_F(Subnet4ParserTest, parseWithInvalidRenewRebind) { std::string config = "{\n" " \"id\": 1,\n" " \"subnet\": \"10.1.2.0/24\",\n" " \"valid-lifetime\": 399,\n" " \"rebind-timer\": 199,\n" " \"renew-timer\": 200\n" "}"; // Basic configuration for subnet4 but with a renew-timer value // larger than rebind-timer. ElementPtr config_element = Element::fromJSON(config); // Parse configuration specified above. Subnet4ConfigParser parser(false); Subnet4Ptr subnet; // Parser should not throw. ASSERT_NO_THROW(subnet = parser.parse(config_element)); ASSERT_TRUE(subnet); // Veriy we emitted the proper log message. addString("DHCPSRV_CFGMGR_RENEW_GTR_REBIND in subnet-id 1," " the value of renew-timer 200 is greater than the value" " of rebind-timer 199, ignoring renew-timer"); EXPECT_TRUE(checkFile()); } } // end of anonymous namespace