From: Razvan Becheriu Date: Wed, 4 Mar 2026 14:19:14 +0000 (+0200) Subject: [#4360] added unit tests X-Git-Tag: Kea-3.1.7~8 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=0867a036b6d7ce4cddfe08080fbac2fc88bb8a4e;p=thirdparty%2Fkea.git [#4360] added unit tests --- diff --git a/src/lib/dhcp/iface_mgr.cc b/src/lib/dhcp/iface_mgr.cc index 0f35131fa2..5a1298603d 100644 --- a/src/lib/dhcp/iface_mgr.cc +++ b/src/lib/dhcp/iface_mgr.cc @@ -711,7 +711,7 @@ IfaceMgr::openSockets6(const uint16_t port, // with interface with 2 global addresses, we would bind 3 sockets // (one for link-local and two for global). That would result in // getting each message 3 times. - if (!addr.get().isV6LinkLocal()){ + if (!addr.get().isV6LinkLocal()) { continue; } @@ -726,7 +726,7 @@ IfaceMgr::openSockets6(const uint16_t port, // the interface actually has opened sockets by checking the number // of sockets to be non zero. if (!skip_opened || !IfaceMgr::hasOpenSocket(addr) || - !iface->getSockets().size()) { + iface->getSockets().size() <= iface->getUnicasts().size()) { try { // Pass a null pointer as an error handler to avoid // suppressing an exception in a system-specific function. diff --git a/src/lib/dhcpsrv/tests/cfg_iface_unittest.cc b/src/lib/dhcpsrv/tests/cfg_iface_unittest.cc index 628f9047d2..7d24c78a4c 100644 --- a/src/lib/dhcpsrv/tests/cfg_iface_unittest.cc +++ b/src/lib/dhcpsrv/tests/cfg_iface_unittest.cc @@ -751,8 +751,8 @@ TEST_F(CfgIfaceTest, retryOpenServiceSockets4OmitBound) { // binding in a single attempt. // Don't check the waiting time for initial calls. - // iface: eth0 addr: 10.0.0.1 port: 67 rbcast: 0 sbcast: 0 - // iface: eth1 addr: 192.0.2.3 port: 67 rbcast: 0 sbcast: 0 - fails + // iface: eth0 addr: 10.0.0.1 port: 67 rbcast: 0 sbcast: 0 - fails + // iface: eth1 addr: 192.0.2.3 port: 67 rbcast: 0 sbcast: 0 if (total_calls > 1) { auto interval = now - last_call_time; auto interval_ms = @@ -809,6 +809,178 @@ TEST_F(CfgIfaceTest, retryOpenServiceSockets4OmitBound) { EXPECT_EQ(exp_calls, total_calls); } +// This test verifies that if any IPv4 socket fails to bind, the opening will +// retry, but the opened sockets will not be re-bound. +TEST_F(CfgIfaceTest, retryOpenServiceSockets4OmitBound2) { + CfgIface cfg4; + + ASSERT_NO_THROW(cfg4.use(AF_INET, "eth0/10.0.0.1")); + ASSERT_NO_THROW(cfg4.use(AF_INET, "eth1")); + + // Parameters + const uint16_t RETRIES = 5; + const uint16_t WAIT_TIME = 5; // miliseconds + + // Require retry socket binding + cfg4.setServiceSocketsMaxRetries(RETRIES); + cfg4.setServiceSocketsRetryWaitTime(WAIT_TIME); + + // For eth0 interface perform only init open, + // for eth1 interface perform 1 init open and a retry. + size_t exp_calls = 4; + + // Set the callback to count calls and check wait time + size_t total_calls = 0; + std::chrono::system_clock::time_point last_call_time; // epoch zero + + auto open_callback = [&total_calls, &last_call_time, WAIT_TIME](uint16_t) { + auto now = std::chrono::system_clock::now(); + bool is_eth1 = total_calls == 2; + + // Skip the wait time check for the socket when two sockets are + // binding in a single attempt. + + // Don't check the waiting time for initial calls. + // iface: eth0 addr: 10.0.0.1 port: 67 rbcast: 0 sbcast: 0 + // iface: eth1 addr: 192.0.2.3 port: 67 rbcast: 0 sbcast: 0 + // iface: eth1 addr: 192.0.2.5 port: 67 rbcast: 0 sbcast: 0 - fails + if (total_calls > 2) { + auto interval = now - last_call_time; + auto interval_ms = + std::chrono::duration_cast( + interval + ).count(); + + EXPECT_GE(interval_ms, WAIT_TIME); + } + + last_call_time = now; + + total_calls++; + + // Fail to open a socket on eth1, success for eth0 + if (is_eth1) { + isc_throw(Unexpected, "CfgIfaceTest: cannot open a port"); + } + }; + + boost::shared_ptr filter( + new isc::dhcp::test::PktFilterTestStub() + ); + + filter->setOpenSocketCallback(open_callback); + + ASSERT_TRUE(filter); + ASSERT_NO_THROW(IfaceMgr::instance().setPacketFilter(filter)); + + bool called = false; + CfgIface::OpenSocketsFailedCallback on_fail_callback = + [&called](ReconnectCtlPtr) { + called = true; + }; + + CfgIface::open_sockets_failed_callback_ = on_fail_callback; + + // Open an unavailable port + ASSERT_NO_THROW(cfg4.openSockets(AF_INET, DHCP4_SERVER_PORT)); + + EXPECT_TRUE(TimerMgr::instance()->isTimerRegistered("SocketReopenTimer")); + + // Wait for a finish sockets binding (with a safe margin). + doWait(RETRIES * WAIT_TIME * 10); + + EXPECT_FALSE(TimerMgr::instance()->isTimerRegistered("SocketReopenTimer")); + EXPECT_FALSE(called); + + EXPECT_EQ(exp_calls, total_calls); +} + +// This test verifies that if any IPv4 socket fails to bind, the opening will +// retry, but the opened sockets will not be re-bound. +TEST_F(CfgIfaceTest, retryOpenServiceSockets4OmitBound3) { + CfgIface cfg4; + + ASSERT_NO_THROW(cfg4.use(AF_INET, "eth0/10.0.0.1")); + ASSERT_NO_THROW(cfg4.use(AF_INET, "eth1")); + + // Parameters + const uint16_t RETRIES = 5; + const uint16_t WAIT_TIME = 5; // miliseconds + + // Require retry socket binding + cfg4.setServiceSocketsMaxRetries(RETRIES); + cfg4.setServiceSocketsRetryWaitTime(WAIT_TIME); + + // For eth0 interface perform only init open, + // for eth1 interface perform 1 init open and a retry. + size_t exp_calls = 4; + + // Set the callback to count calls and check wait time + size_t total_calls = 0; + std::chrono::system_clock::time_point last_call_time; // epoch zero + + auto open_callback = [&total_calls, &last_call_time, WAIT_TIME](uint16_t) { + auto now = std::chrono::system_clock::now(); + bool is_eth1 = total_calls == 1; + + // Skip the wait time check for the socket when two sockets are + // binding in a single attempt. + + // Don't check the waiting time for initial calls. + // iface: eth0 addr: 10.0.0.1 port: 67 rbcast: 0 sbcast: 0 + // iface: eth1 addr: 192.0.2.3 port: 67 rbcast: 0 sbcast: 0 - fails + // iface: eth1 addr: 192.0.2.5 port: 67 rbcast: 0 sbcast: 0 + if (total_calls > 2) { + auto interval = now - last_call_time; + auto interval_ms = + std::chrono::duration_cast( + interval + ).count(); + + EXPECT_GE(interval_ms, WAIT_TIME); + } + + last_call_time = now; + + total_calls++; + + // Fail to open a socket on eth1, success for eth0 + if (is_eth1) { + isc_throw(Unexpected, "CfgIfaceTest: cannot open a port"); + } + }; + + boost::shared_ptr filter( + new isc::dhcp::test::PktFilterTestStub() + ); + + filter->setOpenSocketCallback(open_callback); + + ASSERT_TRUE(filter); + ASSERT_NO_THROW(IfaceMgr::instance().setPacketFilter(filter)); + + bool called = false; + CfgIface::OpenSocketsFailedCallback on_fail_callback = + [&called](ReconnectCtlPtr) { + called = true; + }; + + CfgIface::open_sockets_failed_callback_ = on_fail_callback; + + // Open an unavailable port + ASSERT_NO_THROW(cfg4.openSockets(AF_INET, DHCP4_SERVER_PORT)); + + EXPECT_TRUE(TimerMgr::instance()->isTimerRegistered("SocketReopenTimer")); + + // Wait for a finish sockets binding (with a safe margin). + doWait(RETRIES * WAIT_TIME * 10); + + EXPECT_FALSE(TimerMgr::instance()->isTimerRegistered("SocketReopenTimer")); + EXPECT_FALSE(called); + + EXPECT_EQ(exp_calls, total_calls); +} + // This test verifies that if any IPv4 socket fails to bind, the opening will // retry, but no sockets will be opened for a new interface. TEST_F(CfgIfaceTest, retryOpenServiceSockets4OmitNewInterfaces) { @@ -1162,6 +1334,210 @@ TEST_F(CfgIfaceTest, retryOpenServiceSockets6OmitBound) { EXPECT_EQ(exp_calls, total_calls); } +// This test verifies that if any IPv6 socket fails to bind, the opening will +// retry, but the opened sockets will not be re-bound. +TEST_F(CfgIfaceTest, retryOpenServiceSockets6OmitBound2) { + CfgIface cfg6; + + ASSERT_NO_THROW(cfg6.use(AF_INET6, "eth0/2001:db8:1::1")); + ASSERT_NO_THROW(cfg6.use(AF_INET6, "eth1")); + + // Parameters + const uint16_t RETRIES = 5; + const uint16_t WAIT_TIME = 5; // miliseconds + + // Require retry socket binding + cfg6.setServiceSocketsMaxRetries(RETRIES); + cfg6.setServiceSocketsRetryWaitTime(WAIT_TIME); + +#if defined OS_LINUX + const uint32_t opened_by_eth0 = 4; +#else + const uint32_t opened_by_eth0 = 2; +#endif + +#if defined OS_LINUX + const uint32_t opened_by_eth1 = 3; +#else + const uint32_t opened_by_eth1 = 1; +#endif + + // For eth0 interface perform 2 init open and a retry (opening 4 on Linux systems or 2 otherwise), + // for eth1 interface perform only one init open (opening 3 on Linux systems or 1 otherwise). + size_t exp_calls = 2 + opened_by_eth0 + opened_by_eth1; + + // Set the callback to count calls and check wait time + size_t total_calls = 0; + std::chrono::system_clock::time_point last_call_time; // epoch zero + + auto open_callback = [&total_calls, &last_call_time, WAIT_TIME](uint16_t) { + auto now = std::chrono::system_clock::now(); + bool is_eth0 = total_calls < 2; + + // Skip the wait time check for the socket when two sockets are + // binding in a single attempt. + + // Don't check the waiting time for initial calls. + // iface: eth0 addr: 2001:db8:1::1 port: 547 multicast: 0 - fails + // iface: eth0 addr: fe80::3a60:77ff:fed5:cdef port: 547 multicast: 1 - fails + // iface: eth0 addr: ff02::1:2 port: 547 multicast: 0 --- only on Linux systems - fails + // iface: eth0 addr: ff05::1:3 port: 547 multicast: 0 --- only on Linux systems - fails + // iface: eth1 addr: fe80::3a60:77ff:fed5:abcd port: 547 multicast: 1 + // iface: eth1 addr: ff02::1:2 port: 547 multicast: 0 --- only on Linux systems + // iface: eth1 addr: ff05::1:3 port: 547 multicast: 0 --- only on Linux systems + if (total_calls == 2 + opened_by_eth1) { + auto interval = now - last_call_time; + auto interval_ms = + std::chrono::duration_cast( + interval + ).count(); + + EXPECT_GE(interval_ms, WAIT_TIME); + } + + last_call_time = now; + + total_calls++; + + // Fail to open a socket on eth1, success for eth0 + if (is_eth0) { + isc_throw(Unexpected, "CfgIfaceTest: cannot open a port"); + } + }; + + boost::shared_ptr filter( + new isc::dhcp::test::PktFilter6TestStub() + ); + + filter->setOpenSocketCallback(open_callback); + + ASSERT_TRUE(filter); + ASSERT_NO_THROW(IfaceMgr::instance().setPacketFilter(filter)); + + bool called = false; + CfgIface::OpenSocketsFailedCallback on_fail_callback = + [&called](ReconnectCtlPtr) { + called = true; + }; + + CfgIface::open_sockets_failed_callback_ = on_fail_callback; + + // Open an unavailable port + ASSERT_NO_THROW(cfg6.openSockets(AF_INET6, DHCP6_SERVER_PORT)); + + EXPECT_TRUE(TimerMgr::instance()->isTimerRegistered("SocketReopenTimer")); + + // Wait for a finish sockets binding (with a safe margin). + doWait(RETRIES * WAIT_TIME * 10); + + EXPECT_FALSE(TimerMgr::instance()->isTimerRegistered("SocketReopenTimer")); + EXPECT_FALSE(called); + + EXPECT_EQ(exp_calls, total_calls); +} + +// This test verifies that if any IPv6 socket fails to bind, the opening will +// retry, but the opened sockets will not be re-bound. +TEST_F(CfgIfaceTest, retryOpenServiceSockets6OmitBound3) { + CfgIface cfg6; + + ASSERT_NO_THROW(cfg6.use(AF_INET6, "eth0/2001:db8:1::1")); + ASSERT_NO_THROW(cfg6.use(AF_INET6, "eth1")); + + // Parameters + const uint16_t RETRIES = 5; + const uint16_t WAIT_TIME = 5; // miliseconds + + // Require retry socket binding + cfg6.setServiceSocketsMaxRetries(RETRIES); + cfg6.setServiceSocketsRetryWaitTime(WAIT_TIME); + +#if defined OS_LINUX + const uint32_t opened_by_eth0 = 4; +#else + const uint32_t opened_by_eth0 = 2; +#endif + +#if defined OS_LINUX + const uint32_t opened_by_eth1 = 3; +#else + const uint32_t opened_by_eth1 = 1; +#endif + + // For eth0 interface perform 2 init open and a retry (opening 3 on Linux systems or 1 otherwise), + // for eth1 interface perform only one init open (opening 3 on Linux systems or 1 otherwise). + size_t exp_calls = 2 * opened_by_eth0 - 1 + opened_by_eth1; + + // Set the callback to count calls and check wait time + size_t total_calls = 0; + std::chrono::system_clock::time_point last_call_time; // epoch zero + + auto open_callback = [&total_calls, &last_call_time, WAIT_TIME](uint16_t) { + auto now = std::chrono::system_clock::now(); + bool is_eth0 = total_calls == opened_by_eth0 - 1; + + // Skip the wait time check for the socket when two sockets are + // binding in a single attempt. + + // Don't check the waiting time for initial calls. + // iface: eth0 addr: 2001:db8:1::1 port: 547 multicast: 0 + // iface: eth0 addr: fe80::3a60:77ff:fed5:cdef port: 547 multicast: 1 - fails + // iface: eth0 addr: ff02::1:2 port: 547 multicast: 0 --- only on Linux systems - fails + // iface: eth0 addr: ff05::1:3 port: 547 multicast: 0 --- only on Linux systems - fails + // iface: eth1 addr: fe80::3a60:77ff:fed5:abcd port: 547 multicast: 1 + // iface: eth1 addr: ff02::1:2 port: 547 multicast: 0 --- only on Linux systems + // iface: eth1 addr: ff05::1:3 port: 547 multicast: 0 --- only on Linux systems + if (total_calls == opened_by_eth0 + opened_by_eth1) { + auto interval = now - last_call_time; + auto interval_ms = + std::chrono::duration_cast( + interval + ).count(); + + EXPECT_GE(interval_ms, WAIT_TIME); + } + + last_call_time = now; + + total_calls++; + + // Fail to open a socket on eth1, success for eth0 + if (is_eth0) { + isc_throw(Unexpected, "CfgIfaceTest: cannot open a port"); + } + }; + + boost::shared_ptr filter( + new isc::dhcp::test::PktFilter6TestStub() + ); + + filter->setOpenSocketCallback(open_callback); + + ASSERT_TRUE(filter); + ASSERT_NO_THROW(IfaceMgr::instance().setPacketFilter(filter)); + + bool called = false; + CfgIface::OpenSocketsFailedCallback on_fail_callback = + [&called](ReconnectCtlPtr) { + called = true; + }; + + CfgIface::open_sockets_failed_callback_ = on_fail_callback; + + // Open an unavailable port + ASSERT_NO_THROW(cfg6.openSockets(AF_INET6, DHCP6_SERVER_PORT)); + + EXPECT_TRUE(TimerMgr::instance()->isTimerRegistered("SocketReopenTimer")); + + // Wait for a finish sockets binding (with a safe margin). + doWait(RETRIES * WAIT_TIME * 10); + + EXPECT_FALSE(TimerMgr::instance()->isTimerRegistered("SocketReopenTimer")); + EXPECT_FALSE(called); + + EXPECT_EQ(exp_calls, total_calls); +} + // This test verifies that if any IPv6 socket fails to bind, the opening will // retry, but no sockets will be opened for a new interface. TEST_F(CfgIfaceTest, retryOpenServiceSockets6OmitNewInterfaces) {