]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#1716] Unit tests for open sockets
authorSlawek Figiel <slawek@isc.org>
Fri, 25 Mar 2022 15:46:10 +0000 (16:46 +0100)
committerRazvan Becheriu <razvan@isc.org>
Mon, 4 Apr 2022 14:46:44 +0000 (17:46 +0300)
src/lib/dhcp/tests/pkt_filter6_test_stub.cc
src/lib/dhcp/tests/pkt_filter6_test_stub.h
src/lib/dhcp/tests/pkt_filter_test_stub.cc
src/lib/dhcp/tests/pkt_filter_test_stub.h
src/lib/dhcpsrv/tests/cfg_iface_unittest.cc
src/lib/dhcpsrv/tests/ifaces_config_parser_unittest.cc

index b9872cb99545977c07be849b26faa9e15cdfe0a1..b5421bab459b145582332d84bc3a5cf4cd7029d2 100644 (file)
@@ -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));
 }
 
index c6ab99c4f1a6c145780ad34e1d2d833b86cbfc84..dfc83e842eea0e84fd151a30355fce07e7226a44 100644 (file)
@@ -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<void()> 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
index 6580401e1187e618a8bdf4f6ec26cfa3f2d457d1..572a031dea81bea0e4e0d43270cf97027dce5a85 100644 (file)
@@ -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));
 }
index dedf6d3336e63a6b838d031ab82f532860d47aaa..b5b1081ed87fe466b11f4bf3c738b1c74f2d58b6 100644 (file)
@@ -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<void()> 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
index b8cc3af3f0265c18237d3fbcec28fadbc5960755..4741820d9a769cca46861ab409a1742009bd7a6c 100644 (file)
@@ -7,6 +7,8 @@
 #include <config.h>
 #include <dhcp/dhcp4.h>
 #include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcp/tests/pkt_filter_test_stub.h>
+#include <dhcp/tests/pkt_filter6_test_stub.h>
 #include <dhcpsrv/cfg_iface.h>
 #include <testutils/test_to_element.h>
 #include <gtest/gtest.h>
@@ -508,6 +510,103 @@ TEST_F(CfgIfaceTest, unparse) {
     runToElementTest<CfgIface>(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<isc::dhcp::test::PktFilterTestStub> filter(new isc::dhcp::test::PktFilterTestStub());
+    boost::shared_ptr<isc::dhcp::test::PktFilter6TestStub> 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<isc::dhcp::test::PktFilterTestStub> filter(new isc::dhcp::test::PktFilterTestStub());
+    boost::shared_ptr<isc::dhcp::test::PktFilter6TestStub> 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
index 974ba96af6c8b90811b9054656ade23353453882..09df95e3232b61f36f2826f95138d00bd77ca67f 100644 (file)
@@ -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<CfgIface>(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<CfgIface>(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\": [ ],"