diff options
Diffstat (limited to 'src/bin/dhcp4/tests/direct_client_unittest.cc')
-rw-r--r-- | src/bin/dhcp4/tests/direct_client_unittest.cc | 438 |
1 files changed, 438 insertions, 0 deletions
diff --git a/src/bin/dhcp4/tests/direct_client_unittest.cc b/src/bin/dhcp4/tests/direct_client_unittest.cc new file mode 100644 index 0000000..27439e2 --- /dev/null +++ b/src/bin/dhcp4/tests/direct_client_unittest.cc @@ -0,0 +1,438 @@ +// 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 <config.h> +#include <dhcp/iface_mgr.h> +#include <dhcp/pkt4.h> +#include <dhcp/classify.h> +#include <dhcp/tests/iface_mgr_test_config.h> +#include <dhcpsrv/cfgmgr.h> +#include <dhcpsrv/cfg_subnets4.h> +#include <dhcpsrv/lease_mgr_factory.h> +#include <dhcpsrv/subnet.h> +#include <dhcp4/json_config_parser.h> +#include <dhcp4/tests/dhcp4_client.h> +#include <dhcp4/tests/dhcp4_test_utils.h> +#include <gtest/gtest.h> +#include <string> + +using namespace isc; +using namespace isc::asiolink; +using namespace isc::data; +using namespace isc::dhcp; +using namespace isc::dhcp::test; + +namespace { + +/// @brief Test fixture class for testing message processing from directly +/// connected clients. +/// +/// This class provides mechanisms for testing processing of DHCPv4 messages +/// from directly connected clients. +class DirectClientTest : public Dhcpv4SrvTest { +public: + /// @brief Constructor. + /// + /// Initializes DHCPv4 server object used by various tests. + DirectClientTest(); + + /// @brief Configures the server with one subnet. + /// + /// This creates new configuration for the DHCPv4 with one subnet having + /// a specified prefix. + /// + /// The subnet parameters (such as options, timers etc.) are arbitrarily + /// selected. The subnet and pool mask is always /24. The real configuration + /// would exclude .0 (network address) and .255 (broadcast address), but we + /// ignore that fact for the sake of test simplicity. + /// + /// @param prefix Prefix for a subnet. + void configureSubnet(const std::string& prefix); + + /// @brief Configures the server with two subnets. + /// + /// This function configures DHCPv4 server with two different subnets. + /// The subnet parameters (such as options, timers etc.) are arbitrarily + /// selected. The subnet and pool mask is /24. The real configuration + /// would exclude .0 (network address) and .255 (broadcast address), but we + /// ignore that fact for the sake of test simplicity. + /// + /// @param prefix1 Prefix of the first subnet to be configured. + /// @param prefix2 Prefix of the second subnet to be configured. + void configureTwoSubnets(const std::string& prefix1, + const std::string& prefix2); + + /// @brief Creates simple message from a client. + /// + /// This function creates a DHCPv4 message having a specified type + /// (e.g. Discover, Request) and sets some properties of this + /// message: client identifier, address and interface. The copy of + /// this message is then created by parsing wire data of the original + /// message. This simulates the case when the message is received and + /// parsed by the server. + /// + /// @param msg_type Type of the message to be created. + /// @param iface Name of the interface on which the message has been + /// "received" by the server. + /// @param ifindex Index of the interface on which the message has been + /// "received" by the server. + /// + /// @return Generated message. + Pkt4Ptr createClientMessage(const uint16_t msg_type, + const std::string& iface, + const unsigned int ifindex); + + /// @brief Creates simple message from a client. + /// + /// This function configures a client's message by adding client identifier, + /// setting interface and addresses. The copy of this message is then + /// created by parsing wire data of the original message. This simulates the + /// case when the message is received and parsed by the server. + /// + /// @param msg Caller supplied message to be configured. This object must + /// not be NULL. + /// @param iface Name of the interface on which the message has been + /// "received" by the server. + /// @param ifindex Index of the interface on which the message has been + /// "received" by the server. + /// + /// @return Configured and parsed message. + Pkt4Ptr createClientMessage(const Pkt4Ptr &msg, + const std::string& iface, + const unsigned int ifindex); + + /// @brief This test checks that the message from directly connected client + /// is processed and that client is offered IPv4 address from the subnet + /// which is suitable for the local interface on which the client's message + /// is received. This test uses two subnets, with two active interfaces + /// which IP addresses belong to these subnets. The address offered to the + /// client which message has been sent over eth0 should belong to a + /// different subnet than the address offered for the client sending its + /// message via eth1. + void twoSubnets(); + + /// @brief This test checks that server selects a subnet when receives a + /// message through an interface for which the subnet has been configured. + /// This interface has IPv4 address assigned which belongs to this subnet. + /// This test also verifies that when the message is received through the + /// interface for which there is no suitable subnet, the message is + /// discarded. + void oneSubnet(); + + /// @brief This test verifies that the server uses ciaddr to select a subnet + /// for a client which renews its lease. + void renew(); + + /// This test verifies that when a client in the Rebinding state broadcasts + /// a Request message through an interface for which a subnet is configured, + /// the server responds to this Request. It also verifies that when such a + /// Request is sent through the interface for which there is no subnet + /// configured the client's message is discarded. + void rebind(); + + /// @brief classes the client belongs to + /// + /// This is empty in most cases, but it is needed as a parameter for all + /// getSubnet4() calls. + ClientClasses classify_; +}; + +DirectClientTest::DirectClientTest() : Dhcpv4SrvTest() { +} + +void +DirectClientTest::configureSubnet(const std::string& prefix) { + std::ostringstream config; + config << "{ \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + "}," + "\"rebind-timer\": 2000, " + "\"renew-timer\": 1000, " + "\"option-data\": [ ]," + "\"subnet4\": [ { " + " \"id\": 1, " + " \"pools\": [ { \"pool\": \"" << prefix << "/24\" } ]," + " \"subnet\": \"" << prefix << "/24\", " + " \"rebind-timer\": 2000, " + " \"renew-timer\": 1000, " + " \"valid-lifetime\": 4000" + "} ]," + "\"valid-lifetime\": 4000 }"; + + configure(config.str()); +} + +void +DirectClientTest::configureTwoSubnets(const std::string& prefix1, + const std::string& prefix2) { + std::ostringstream config; + config << "{ \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + "}," + "\"rebind-timer\": 2000, " + "\"renew-timer\": 1000, " + "\"option-data\": [ ]," + "\"subnet4\": [ { " + " \"id\": 1, " + " \"pools\": [ { \"pool\": \"" << prefix1 << "/24\" } ]," + " \"subnet\": \"" << prefix1 << "/24\", " + " \"rebind-timer\": 2000, " + " \"renew-timer\": 1000, " + " \"valid-lifetime\": 4000" + " }," + "{ " + " \"id\": 2, " + " \"pools\": [ { \"pool\": \"" << prefix2 << "/24\" } ]," + " \"subnet\": \"" << prefix2 << "/24\", " + " \"rebind-timer\": 2000, " + " \"renew-timer\": 1000, " + " \"valid-lifetime\": 4000" + "} ]," + "\"valid-lifetime\": 4000 }"; + + configure(config.str()); +} + +Pkt4Ptr +DirectClientTest::createClientMessage(const uint16_t msg_type, + const std::string& iface, + const unsigned int ifindex) { + // Create a source packet. + Pkt4Ptr msg = Pkt4Ptr(new Pkt4(msg_type, 1234)); + return (createClientMessage(msg, iface, ifindex)); + +} + +Pkt4Ptr +DirectClientTest::createClientMessage(const Pkt4Ptr& msg, + const std::string& iface, + const unsigned int ifindex) { + msg->setRemoteAddr(IOAddress("255.255.255.255")); + msg->addOption(generateClientId()); + msg->setIface(iface); + msg->setIndex(ifindex); + + // Create copy of this packet by parsing its wire data. Make sure that the + // local and remote address are set like it was a message sent from the + // directly connected client. + Pkt4Ptr received; + createPacketFromBuffer(msg, received); + received->setIface(iface); + received->setIndex(ifindex); + received->setLocalAddr(IOAddress("255.255.255.255")); + received->setRemoteAddr(IOAddress("0.0.0.0")); + + return (received); +} + +void +DirectClientTest::twoSubnets() { + // Configure IfaceMgr with fake interfaces lo, eth0 and eth1. + IfaceMgrTestConfig iface_config(true); + // After creating interfaces we have to open sockets as it is required + // by the message processing code. + ASSERT_NO_THROW(IfaceMgr::instance().openSockets4()); + // Add two subnets: address on eth0 belongs to the second subnet, + // address on eth1 belongs to the first subnet. + ASSERT_NO_FATAL_FAILURE(configureTwoSubnets("192.0.2.0", "10.0.0.0")); + // Create Discover and simulate reception of this message through eth0. + Pkt4Ptr dis = createClientMessage(DHCPDISCOVER, "eth0", ETH0_INDEX); + srv_.fakeReceive(dis); + // Create Request and simulate reception of this message through eth1. + Pkt4Ptr req = createClientMessage(DHCPREQUEST, "eth1", ETH1_INDEX); + srv_.fakeReceive(req); + + // Process clients' messages. + srv_.run(); + + // Check that the server did send responses. + ASSERT_EQ(2, srv_.fake_sent_.size()); + + // In multi-threading responses can be received out of order. + Pkt4Ptr offer; + Pkt4Ptr ack; + + while (srv_.fake_sent_.size()) { + // Make sure that we received a response. + Pkt4Ptr response = srv_.fake_sent_.front(); + ASSERT_TRUE(response); + srv_.fake_sent_.pop_front(); + + if (response->getType() == DHCPOFFER) { + offer = response; + } else if (response->getType() == DHCPACK) { + ack = response; + } + } + + // Client should get an Offer (not a NAK). + ASSERT_TRUE(offer); + + // Client should get an Ack (not a NAK). + ASSERT_TRUE(ack); + + // Check that the offered address belongs to the suitable subnet. + Subnet4Ptr subnet = CfgMgr::instance().getCurrentCfg()-> + getCfgSubnets4()->selectSubnet(offer->getYiaddr()); + ASSERT_TRUE(subnet); + EXPECT_EQ("10.0.0.0", subnet->get().first.toText()); + + + // Check that the offered address belongs to the suitable subnet. + subnet = CfgMgr::instance().getCurrentCfg()-> + getCfgSubnets4()->selectSubnet(ack->getYiaddr()); + ASSERT_TRUE(subnet); + EXPECT_EQ("192.0.2.0", subnet->get().first.toText()); +} + +TEST_F(DirectClientTest, twoSubnets) { + Dhcpv4SrvMTTestGuard guard(*this, false); + twoSubnets(); +} + +TEST_F(DirectClientTest, twoSubnetsMultiThreading) { + Dhcpv4SrvMTTestGuard guard(*this, true); + twoSubnets(); +} + +void +DirectClientTest::oneSubnet() { + // Configure IfaceMgr with fake interfaces lo, eth0 and eth1. + IfaceMgrTestConfig iface_config(true); + // After creating interfaces we have to open sockets as it is required + // by the message processing code. + ASSERT_NO_THROW(IfaceMgr::instance().openSockets4()); + // Add a subnet which will be selected when a message from directly + // connected client is received through interface eth0. + ASSERT_NO_FATAL_FAILURE(configureSubnet("10.0.0.0")); + // Create Discover and simulate reception of this message through eth0. + Pkt4Ptr dis = createClientMessage(DHCPDISCOVER, "eth0", ETH0_INDEX); + srv_.fakeReceive(dis); + // Create Request and simulate reception of this message through eth1. + Pkt4Ptr req = createClientMessage(DHCPDISCOVER, "eth1", ETH1_INDEX); + srv_.fakeReceive(req); + + // Process clients' messages. + srv_.run(); + + // Check that the server sent one response for the message received + // through eth0. The other client's message should be discarded. + ASSERT_EQ(1, srv_.fake_sent_.size()); + + // Check the response. The first Discover was sent via eth0 for which + // the subnet has been configured. + Pkt4Ptr response = srv_.fake_sent_.front(); + ASSERT_TRUE(response); + srv_.fake_sent_.pop_front(); + + // Since Discover has been received through the interface for which + // the subnet has been configured, the server should respond with + // an Offer message. + ASSERT_EQ(DHCPOFFER, response->getType()); + // Check that the offered address belongs to the suitable subnet. + Subnet4Ptr subnet = CfgMgr::instance().getCurrentCfg()-> + getCfgSubnets4()->selectSubnet(response->getYiaddr()); + ASSERT_TRUE(subnet); + EXPECT_EQ("10.0.0.0", subnet->get().first.toText()); +} + +TEST_F(DirectClientTest, oneSubnet) { + Dhcpv4SrvMTTestGuard guard(*this, false); + oneSubnet(); +} + +TEST_F(DirectClientTest, oneSubnetMultiThreading) { + Dhcpv4SrvMTTestGuard guard(*this, true); + oneSubnet(); +} + +void +DirectClientTest::renew() { + // Configure IfaceMgr with fake interfaces lo, eth0 and eth1. + IfaceMgrTestConfig iface_config(true); + // After creating interfaces we have to open sockets as it is required + // by the message processing code. + ASSERT_NO_THROW(IfaceMgr::instance().openSockets4()); + // Add a subnet. + ASSERT_NO_FATAL_FAILURE(configureSubnet("10.0.0.0")); + + // Create the DHCPv4 client. + Dhcp4Client client; + client.useRelay(false); + + // Obtain the lease using the 4-way exchange. + ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<IOAddress>(new IOAddress("10.0.0.10")))); + ASSERT_EQ("10.0.0.10", client.config_.lease_.addr_.toText()); + + // Put the client into the renewing state. + client.setState(Dhcp4Client::RENEWING); + + // Renew, and make sure we have obtained the same address. + ASSERT_NO_THROW(client.doRequest()); + ASSERT_TRUE(client.getContext().response_); + EXPECT_EQ(DHCPACK, static_cast<int>(client.getContext().response_->getType())); + EXPECT_EQ("10.0.0.10", client.config_.lease_.addr_.toText()); +} + +TEST_F(DirectClientTest, renew) { + Dhcpv4SrvMTTestGuard guard(*this, false); + renew(); +} + +TEST_F(DirectClientTest, renewMultiThreading) { + Dhcpv4SrvMTTestGuard guard(*this, true); + renew(); +} + +void +DirectClientTest::rebind() { + // Configure IfaceMgr with fake interfaces lo, eth0 and eth1. + IfaceMgrTestConfig iface_config(true); + // After creating interfaces we have to open sockets as it is required + // by the message processing code. + ASSERT_NO_THROW(IfaceMgr::instance().openSockets4()); + // Add a subnet. + ASSERT_NO_FATAL_FAILURE(configureSubnet("10.0.0.0")); + + // Create the DHCPv4 client. + Dhcp4Client client; + client.useRelay(false); + + // Obtain the lease using the 4-way exchange. + ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<IOAddress>(new IOAddress("10.0.0.10")))); + ASSERT_EQ("10.0.0.10", client.config_.lease_.addr_.toText()); + + // Put the client into the rebinding state. + client.setState(Dhcp4Client::REBINDING); + + // Broadcast Request through an interface for which there is no subnet + // configured. This message should be discarded by the server. + client.setIfaceName("eth1"); + client.setIfaceIndex(ETH1_INDEX); + ASSERT_NO_THROW(client.doRequest()); + EXPECT_FALSE(client.getContext().response_); + + // Send Rebind over the correct interface, and make sure we have obtained + // the same address. + client.setIfaceName("eth0"); + client.setIfaceIndex(ETH0_INDEX); + ASSERT_NO_THROW(client.doRequest()); + ASSERT_TRUE(client.getContext().response_); + EXPECT_EQ(DHCPACK, static_cast<int>(client.getContext().response_->getType())); + EXPECT_EQ("10.0.0.10", client.config_.lease_.addr_.toText()); +} + +TEST_F(DirectClientTest, rebind) { + Dhcpv4SrvMTTestGuard guard(*this, false); + rebind(); +} + +TEST_F(DirectClientTest, rebindMultiThreading) { + Dhcpv4SrvMTTestGuard guard(*this, true); + rebind(); +} + +} |