From: Slawek Figiel Date: Fri, 25 Mar 2022 15:46:10 +0000 (+0100) Subject: [#1716] Unit tests for open sockets X-Git-Tag: Kea-2.1.5~88 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c59ce0d731a42e14c12c1caa629116a5a833b7d7;p=thirdparty%2Fkea.git [#1716] Unit tests for open sockets --- diff --git a/src/lib/dhcp/tests/pkt_filter6_test_stub.cc b/src/lib/dhcp/tests/pkt_filter6_test_stub.cc index b9872cb995..b5421bab45 100644 --- a/src/lib/dhcp/tests/pkt_filter6_test_stub.cc +++ b/src/lib/dhcp/tests/pkt_filter6_test_stub.cc @@ -20,6 +20,10 @@ SocketInfo PktFilter6TestStub::openSocket(const Iface&, const isc::asiolink::IOAddress& addr, const uint16_t port, const bool) { + if (open_socket_callback_ != nullptr) { + open_socket_callback_(); + } + return (SocketInfo(addr, port, 0)); } diff --git a/src/lib/dhcp/tests/pkt_filter6_test_stub.h b/src/lib/dhcp/tests/pkt_filter6_test_stub.h index c6ab99c4f1..dfc83e842e 100644 --- a/src/lib/dhcp/tests/pkt_filter6_test_stub.h +++ b/src/lib/dhcp/tests/pkt_filter6_test_stub.h @@ -16,6 +16,9 @@ namespace isc { namespace dhcp { namespace test { +/// @brief An open socket callback that can be use for a testing purposes. +typedef std::function PktFilter6OpenSocketCallback; + /// @brief A stub implementation of the PktFilter6 class. /// /// This class implements abstract methods of the @c isc::dhcp::PktFilter6 @@ -82,6 +85,14 @@ public: /// @return true if multicast join was successful static bool joinMulticast(int sock, const std::string& ifname, const std::string & mcast); + + /// @brief Set an open socket callback. Use it for testing + // purposes, e.g., counting the number of calls or throwing an exception. + void setOpenSocketCallback(PktFilter6OpenSocketCallback callback) { + open_socket_callback_ = callback; + } +private: + PktFilter6OpenSocketCallback open_socket_callback_{nullptr}; }; } // namespace isc::dhcp::test diff --git a/src/lib/dhcp/tests/pkt_filter_test_stub.cc b/src/lib/dhcp/tests/pkt_filter_test_stub.cc index 6580401e11..572a031dea 100644 --- a/src/lib/dhcp/tests/pkt_filter_test_stub.cc +++ b/src/lib/dhcp/tests/pkt_filter_test_stub.cc @@ -34,6 +34,10 @@ PktFilterTestStub::openSocket(Iface&, isc_throw(Unexpected, "PktFilterTestStub: cannot open /dev/null:" << errmsg); } + + if (open_socket_callback_ != nullptr) { + open_socket_callback_(); + } return (SocketInfo(addr, port, fd)); } diff --git a/src/lib/dhcp/tests/pkt_filter_test_stub.h b/src/lib/dhcp/tests/pkt_filter_test_stub.h index dedf6d3336..b5b1081ed8 100644 --- a/src/lib/dhcp/tests/pkt_filter_test_stub.h +++ b/src/lib/dhcp/tests/pkt_filter_test_stub.h @@ -16,6 +16,9 @@ namespace isc { namespace dhcp { namespace test { +/// @brief An open socket callback that can be use for a testing purposes. +typedef std::function PktFilterOpenSocketCallback; + /// @brief A stub implementation of the PktFilter class. /// /// This class implements abstract methods of the @c isc::dhcp::PktFilter @@ -89,7 +92,15 @@ public: // Change the scope of the protected function so as they can be unit tested. using PktFilter::openFallbackSocket; + /// @brief Set an open socket callback. Use it for testing + // purposes, e.g., counting the number of calls or throwing an exception. + void setOpenSocketCallback(PktFilterOpenSocketCallback callback) { + open_socket_callback_ = callback; + } + bool direct_response_supported_; +private: + PktFilterOpenSocketCallback open_socket_callback_{nullptr}; }; } // namespace isc::dhcp::test diff --git a/src/lib/dhcpsrv/tests/cfg_iface_unittest.cc b/src/lib/dhcpsrv/tests/cfg_iface_unittest.cc index b8cc3af3f0..4741820d9a 100644 --- a/src/lib/dhcpsrv/tests/cfg_iface_unittest.cc +++ b/src/lib/dhcpsrv/tests/cfg_iface_unittest.cc @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include #include @@ -508,6 +510,103 @@ TEST_F(CfgIfaceTest, unparse) { runToElementTest(expected, cfg6); } +// This test verifies that it is possible to require that all +// service sockets are opened properly. If any socket fails to +// bind then an exception should be thrown. +TEST_F(CfgIfaceTest, requireOpenAllServiceSockets) { + CfgIface cfg4; + CfgIface cfg6; + ASSERT_NO_THROW(cfg4.use(AF_INET, "eth0")); + ASSERT_NO_THROW(cfg4.use(AF_INET, "eth1/192.0.2.3")); + ASSERT_NO_THROW(cfg6.use(AF_INET6, "eth0/2001:db8:1::1")); + ASSERT_NO_THROW(cfg6.use(AF_INET6, "eth1")); + + // Require all sockets bind successfully + cfg4.setServiceSocketsRequireAll(true); + cfg6.setServiceSocketsRequireAll(true); + + // Open an available port + ASSERT_NO_THROW(cfg4.openSockets(AF_INET, DHCP4_SERVER_PORT)); + ASSERT_NO_THROW(cfg6.openSockets(AF_INET6, DHCP6_SERVER_PORT)); + cfg4.closeSockets(); + cfg6.closeSockets(); + + // Set the callback to throw an exception on open + auto open_callback = [](){ + isc_throw(Unexpected, "CfgIfaceTest: cannot open a port"); + }; + boost::shared_ptr filter(new isc::dhcp::test::PktFilterTestStub()); + boost::shared_ptr filter6(new isc::dhcp::test::PktFilter6TestStub()); + filter->setOpenSocketCallback(open_callback); + filter6->setOpenSocketCallback(open_callback); + ASSERT_TRUE(filter); + ASSERT_TRUE(filter6); + ASSERT_NO_THROW(IfaceMgr::instance().setPacketFilter(filter)); + ASSERT_NO_THROW(IfaceMgr::instance().setPacketFilter(filter6)); + + // Open an unavailable port + EXPECT_THROW(cfg4.openSockets(AF_INET, DHCP4_SERVER_PORT), isc::dhcp::SocketConfigError); + EXPECT_THROW(cfg6.openSockets(AF_INET6, DHCP6_SERVER_PORT), isc::dhcp::SocketConfigError); +} + +// This test verifies that if any socket fails to bind, then the opening will retry. +TEST_F(CfgIfaceTest, retryOpenServiceSockets) { + CfgIface cfg4; + CfgIface cfg6; + + ASSERT_NO_THROW(cfg4.use(AF_INET, "eth0")); + ASSERT_NO_THROW(cfg4.use(AF_INET, "eth1/192.0.2.3")); + ASSERT_NO_THROW(cfg6.use(AF_INET6, "eth0/2001:db8:1::1")); + ASSERT_NO_THROW(cfg6.use(AF_INET6, "eth1")); + + const uint16_t RETRIES = 5; + const uint16_t WAIT_TIME = 10; // miliseconds + + // Require retry socket binding + cfg4.setServiceSocketsMaxRetries(RETRIES); + cfg4.setServiceSocketsRetryWaitTime(WAIT_TIME); + cfg6.setServiceSocketsMaxRetries(RETRIES); + cfg6.setServiceSocketsRetryWaitTime(WAIT_TIME); + + // Set the callback to count calls and check wait time + size_t total_calls = 0; + auto last_call_time = std::chrono::system_clock::time_point::min(); + auto open_callback = [&total_calls, &last_call_time, RETRIES, WAIT_TIME](){ + auto now = std::chrono::system_clock::now(); + + // Don't check the waiting time for initial calls as they + // can be done immediately after the last call for the previous socket. + if (total_calls % (RETRIES + 1) != 0) { + auto interval = now - last_call_time; + EXPECT_GE(interval, std::chrono::milliseconds(WAIT_TIME)); + } + + last_call_time = now; + total_calls++; + + // Fail to open a socket + isc_throw(Unexpected, "CfgIfaceTest: cannot open a port"); + }; + boost::shared_ptr filter(new isc::dhcp::test::PktFilterTestStub()); + boost::shared_ptr filter6(new isc::dhcp::test::PktFilter6TestStub()); + filter->setOpenSocketCallback(open_callback); + filter6->setOpenSocketCallback(open_callback); + ASSERT_TRUE(filter); + ASSERT_TRUE(filter6); + ASSERT_NO_THROW(IfaceMgr::instance().setPacketFilter(filter)); + ASSERT_NO_THROW(IfaceMgr::instance().setPacketFilter(filter6)); + + // Open an unavailable port + ASSERT_NO_THROW(cfg4.openSockets(AF_INET, DHCP4_SERVER_PORT)); + ASSERT_NO_THROW(cfg6.openSockets(AF_INET6, DHCP6_SERVER_PORT)); + + // For IPv4 bind to: eth0 and eth1 (2). + // For IPv6 bind to: unicast for eth0 and multicast for eth0 and eth1 (3). + // For each interface perform 1 init open and 5 retries (6). + // Perform 30 open calls ((2+3) * 6). + EXPECT_EQ(30, total_calls); +} + // This test verifies that it is possible to specify the socket // type to be used by the DHCPv4 server. // This test is enabled on LINUX and BSD only, because the diff --git a/src/lib/dhcpsrv/tests/ifaces_config_parser_unittest.cc b/src/lib/dhcpsrv/tests/ifaces_config_parser_unittest.cc index 974ba96af6..09df95e323 100644 --- a/src/lib/dhcpsrv/tests/ifaces_config_parser_unittest.cc +++ b/src/lib/dhcpsrv/tests/ifaces_config_parser_unittest.cc @@ -293,6 +293,7 @@ TEST_F(IfacesConfigParserTest, serviceSocketRequireAll) { CfgIfacePtr cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface(); ASSERT_TRUE(cfg_iface); ASSERT_NO_THROW(parser.parse(cfg_iface, config_element)); + EXPECT_TRUE(cfg_iface->getServiceSocketsRequireAll()); // Check it can be unparsed. runToElementTest(config, *cfg_iface); @@ -316,6 +317,7 @@ TEST_F(IfacesConfigParserTest, serviceSocketMaxRetries) { CfgIfacePtr cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface(); ASSERT_TRUE(cfg_iface); ASSERT_NO_THROW(parser.parse(cfg_iface, config_element)); + EXPECT_FALSE(cfg_iface->getServiceSocketsRequireAll()); // Configuration should contain a number of retries and a wait time. std::string expected_config = "{ \"interfaces\": [ ]," @@ -347,6 +349,7 @@ TEST_F(IfacesConfigParserTest, serviceSocketRetryWaitTime) { CfgIfacePtr cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface(); ASSERT_TRUE(cfg_iface); ASSERT_NO_THROW(parser.parse(cfg_iface, config_element)); + EXPECT_FALSE(cfg_iface->getServiceSocketsRequireAll()); // Check it can be unparsed. runToElementTest(config, *cfg_iface); @@ -371,6 +374,7 @@ TEST_F(IfacesConfigParserTest, serviceSocketRetryWaitTimeWithoutMaxRetries) { CfgIfacePtr cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface(); ASSERT_TRUE(cfg_iface); ASSERT_NO_THROW(parser.parse(cfg_iface, config_element)); + EXPECT_FALSE(cfg_iface->getServiceSocketsRequireAll()); // Retry wait time is not applicable; it is skipped. std::string expected_config = "{ \"interfaces\": [ ],"