summaryrefslogtreecommitdiffstats
path: root/src/lib/dhcpsrv/tests/cfg_iface_unittest.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/dhcpsrv/tests/cfg_iface_unittest.cc')
-rw-r--r--src/lib/dhcpsrv/tests/cfg_iface_unittest.cc1065
1 files changed, 1065 insertions, 0 deletions
diff --git a/src/lib/dhcpsrv/tests/cfg_iface_unittest.cc b/src/lib/dhcpsrv/tests/cfg_iface_unittest.cc
new file mode 100644
index 0000000..03591c0
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/cfg_iface_unittest.cc
@@ -0,0 +1,1065 @@
+// Copyright (C) 2014-2022 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/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 <asiolink/io_service.h>
+#include <asiolink/asio_wrapper.h>
+#include <asiolink/interval_timer.h>
+#include <dhcpsrv/timer_mgr.h>
+#include <testutils/test_to_element.h>
+#include <gtest/gtest.h>
+
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::test;
+using namespace isc::data;
+using namespace isc::util;
+
+namespace {
+
+/// @brief Test fixture class for testing the @c CfgIface class.
+class CfgIfaceTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// By initializing the @c IfaceMgrTestConfig object it creates a set of
+ /// fake interfaces: lo, eth0, eth1.
+ CfgIfaceTest() :
+ iface_mgr_test_config_(true) {
+ }
+
+ /// @brief Checks if socket of the specified family is opened on interface.
+ ///
+ /// @param iface_name Interface name.
+ /// @param family One of: AF_INET or AF_INET6
+ bool socketOpen(const std::string& iface_name, const int family) const;
+
+ /// @brief Checks if socket is opened on the specified interface and bound
+ /// to a specific IPv4 address.
+ ///
+ /// @param iface_name Interface name.
+ /// @param address Address that the socket should be bound to.
+ bool socketOpen(const std::string& iface_name,
+ const std::string& address) const;
+
+ /// @brief Checks if unicast socket is opened on interface.
+ ///
+ /// @param iface_name Interface name.
+ bool unicastOpen(const std::string& iface_name) const;
+
+ /// @brief Wait for specific timeout.
+ ///
+ /// @param timeout Wait timeout in milliseconds.
+ void doWait(const long timeout);
+
+ /// @brief Holds a fake configuration of the interfaces.
+ IfaceMgrTestConfig iface_mgr_test_config_;
+
+ /// @brief Pointer to IO service used by the tests.
+ asiolink::IOServicePtr io_service_;
+
+private:
+
+ /// @brief Prepares the class for a test.
+ virtual void SetUp();
+
+ /// @brief Cleans up after the test.
+ virtual void TearDown();
+};
+
+void
+CfgIfaceTest::SetUp() {
+ IfaceMgr::instance().setTestMode(true);
+ io_service_.reset(new asiolink::IOService());
+ TimerMgr::instance()->setIOService(io_service_);
+}
+
+void
+CfgIfaceTest::TearDown() {
+ // Remove all timers.
+ TimerMgr::instance()->unregisterTimers();
+
+ IfaceMgr::instance().setTestMode(false);
+ IfaceMgr::instance().clearIfaces();
+ IfaceMgr::instance().closeSockets();
+ IfaceMgr::instance().detectIfaces();
+
+ // Reset global handlers
+ CfgIface::open_sockets_failed_callback_ = 0;
+}
+
+bool
+CfgIfaceTest::socketOpen(const std::string& iface_name,
+ const int family) const {
+ return (iface_mgr_test_config_.socketOpen(iface_name, family));
+}
+
+bool
+CfgIfaceTest::socketOpen(const std::string& iface_name,
+ const std::string& address) const {
+ return (iface_mgr_test_config_.socketOpen(iface_name, address));
+}
+
+bool
+CfgIfaceTest::unicastOpen(const std::string& iface_name) const {
+ return (iface_mgr_test_config_.unicastOpen(iface_name));
+}
+
+void
+CfgIfaceTest::doWait(const long timeout) {
+ asiolink::IntervalTimer timer(*io_service_);
+ timer.setup([this]() {
+ io_service_->stop();
+ }, timeout, asiolink::IntervalTimer::ONE_SHOT);
+ io_service_->run();
+ io_service_->get_io_service().reset();
+}
+
+// This test checks that the interface names can be explicitly selected
+// by their names and IPv4 sockets are opened on these interfaces.
+TEST_F(CfgIfaceTest, explicitNamesV4) {
+ CfgIface cfg;
+ // Specify valid interface names. There should be no error.
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth0"));
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth1"));
+
+ // Open sockets on specified interfaces.
+ cfg.openSockets(AF_INET, DHCP4_SERVER_PORT);
+
+ // Sockets should be now open on eth0 and eth1, but not on loopback.
+ EXPECT_TRUE(socketOpen("eth0", AF_INET));
+ EXPECT_TRUE(socketOpen("eth1", AF_INET));
+ EXPECT_FALSE(socketOpen("lo", AF_INET));
+
+ // No IPv6 sockets should be present because we wanted IPv4 sockets.
+ EXPECT_FALSE(socketOpen("eth0", AF_INET6));
+ EXPECT_FALSE(socketOpen("eth1", AF_INET6));
+ EXPECT_FALSE(socketOpen("lo", AF_INET6));
+
+ // Close all sockets and make sure they are really closed.
+ cfg.closeSockets();
+ ASSERT_FALSE(socketOpen("eth0", AF_INET));
+ ASSERT_FALSE(socketOpen("eth1", AF_INET));
+ ASSERT_FALSE(socketOpen("lo", AF_INET));
+
+ // Reset configuration and select only one interface this time.
+ cfg.reset();
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth1"));
+
+ cfg.openSockets(AF_INET, DHCP4_SERVER_PORT);
+
+ // Socket should be open on eth1 only.
+ EXPECT_FALSE(socketOpen("eth0", AF_INET));
+ EXPECT_TRUE(socketOpen("eth1", AF_INET));
+ EXPECT_FALSE(socketOpen("lo", AF_INET));
+}
+
+// This test checks that it is possible to specify an interface and address
+// on this interface to which the socket should be bound. The sockets should
+// not be opened on other addresses on this interface.
+TEST_F(CfgIfaceTest, explicitNamesAndAddressesV4) {
+ CfgIface cfg;
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth0/10.0.0.1"));
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth1/192.0.2.3"));
+
+ // Open sockets on specified interfaces and addresses.
+ cfg.openSockets(AF_INET, DHCP4_SERVER_PORT);
+
+ EXPECT_TRUE(socketOpen("eth0", "10.0.0.1"));
+ EXPECT_TRUE(socketOpen("eth1", "192.0.2.3"));
+ EXPECT_FALSE(socketOpen("eth1", "192.0.2.5"));
+
+ // Close all sockets and make sure they are really closed.
+ cfg.closeSockets();
+ ASSERT_FALSE(socketOpen("eth0", "10.0.0.1"));
+ ASSERT_FALSE(socketOpen("eth1", "192.0.2.3"));
+ ASSERT_FALSE(socketOpen("eth1", "192.0.2.5"));
+
+ // Reset configuration.
+ cfg.reset();
+
+ // Now check that the socket can be bound to a different address on
+ // eth1.
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth1/192.0.2.5"));
+
+ // Open sockets according to the new configuration.
+ cfg.openSockets(AF_INET, DHCP4_SERVER_PORT);
+
+ EXPECT_FALSE(socketOpen("eth0", "10.0.0.1"));
+ EXPECT_FALSE(socketOpen("eth1", "192.0.2.3"));
+ EXPECT_TRUE(socketOpen("eth1", "192.0.2.5"));
+}
+
+// This test checks that the invalid interface name and/or IPv4 address
+// results in error.
+TEST_F(CfgIfaceTest, explicitNamesAndAddressesInvalidV4) {
+ CfgIface cfg;
+ // An address not assigned to the interface.
+ EXPECT_THROW(cfg.use(AF_INET, "eth0/10.0.0.2"), NoSuchAddress);
+ // IPv6 address.
+ EXPECT_THROW(cfg.use(AF_INET, "eth0/2001:db8:1::1"), InvalidIfaceName);
+ // Wildcard interface name with an address.
+ EXPECT_THROW(cfg.use(AF_INET, "*/10.0.0.1"), InvalidIfaceName);
+
+ // Duplicated interface.
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth1"));
+ EXPECT_THROW(cfg.use(AF_INET, "eth1/192.0.2.3"), DuplicateIfaceName);
+}
+
+// This test checks that it is possible to explicitly select multiple
+// IPv4 addresses on a single interface.
+TEST_F(CfgIfaceTest, multipleAddressesSameInterfaceV4) {
+ CfgIface cfg;
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth1/192.0.2.3"));
+ // Cannot add the same address twice.
+ ASSERT_THROW(cfg.use(AF_INET, "eth1/192.0.2.3"), DuplicateAddress);
+ // Can add another address on this interface.
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth1/192.0.2.5"));
+ // Can't select the whole interface.
+ ASSERT_THROW(cfg.use(AF_INET, "eth1"), DuplicateIfaceName);
+
+ cfg.openSockets(AF_INET, DHCP4_SERVER_PORT);
+
+ EXPECT_FALSE(socketOpen("eth0", "10.0.0.1"));
+ EXPECT_TRUE(socketOpen("eth1", "192.0.2.3"));
+ EXPECT_TRUE(socketOpen("eth1", "192.0.2.5"));
+}
+
+// This test checks that it is possible to specify the loopback interface.
+TEST_F(CfgIfaceTest, explicitLoopbackV4) {
+ CfgIface cfg;
+ ASSERT_NO_THROW(cfg.use(AF_INET, "lo"));
+
+ // Use UDP sockets
+ ASSERT_NO_THROW(cfg.useSocketType(AF_INET, CfgIface::SOCKET_UDP));
+
+ // Open sockets on specified interfaces and addresses.
+ cfg.openSockets(AF_INET, DHCP4_SERVER_PORT);
+
+ EXPECT_TRUE(socketOpen("lo", "127.0.0.1"));
+
+ // Close all sockets and make sure they are really closed.
+ cfg.closeSockets();
+ ASSERT_FALSE(socketOpen("lo", "127.0.0.1"));
+
+ // Reset configuration.
+ cfg.reset();
+
+ // Retry with wildcard
+ ASSERT_NO_THROW(cfg.use(AF_INET, "*"));
+ ASSERT_NO_THROW(cfg.use(AF_INET, "lo"));
+ ASSERT_NO_THROW(cfg.useSocketType(AF_INET, CfgIface::SOCKET_UDP));
+ cfg.openSockets(AF_INET, DHCP4_SERVER_PORT);
+ // It is now allowed to use loopback, even with wildcard.
+ EXPECT_TRUE(socketOpen("lo", "127.0.0.1"));
+ cfg.closeSockets();
+ ASSERT_FALSE(socketOpen("lo", "127.0.0.1"));
+
+ // Retry without UDP sockets (lo can be only used with udp sockets)
+ cfg.reset();
+ ASSERT_NO_THROW(cfg.use(AF_INET, "lo"));
+ cfg.openSockets(AF_INET, DHCP4_SERVER_PORT);
+ // No loopback socket
+ EXPECT_FALSE(socketOpen("lo", "127.0.0.1"));
+
+ // Retry with a second interface
+ cfg.reset();
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth0"));
+ ASSERT_NO_THROW(cfg.use(AF_INET, "lo"));
+ ASSERT_NO_THROW(cfg.useSocketType(AF_INET, CfgIface::SOCKET_UDP));
+ cfg.openSockets(AF_INET, DHCP4_SERVER_PORT);
+ // The logic used to require lo to be the only interface. That constraint
+ // was removed.
+ EXPECT_TRUE(socketOpen("lo", "127.0.0.1"));
+ cfg.closeSockets();
+ EXPECT_FALSE(socketOpen("lo", "127.0.0.1"));
+
+ // Finally with interfaces and addresses
+ cfg.reset();
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth0/10.0.0.1"));
+ ASSERT_NO_THROW(cfg.use(AF_INET, "lo/127.0.0.1"));
+ ASSERT_NO_THROW(cfg.useSocketType(AF_INET, CfgIface::SOCKET_UDP));
+ cfg.openSockets(AF_INET, DHCP4_SERVER_PORT);
+ // Only loopback is no longer a constraint
+ EXPECT_TRUE(socketOpen("lo", "127.0.0.1"));
+ cfg.closeSockets();
+ EXPECT_FALSE(socketOpen("lo", "127.0.0.1"));
+}
+
+// This test checks that the interface names can be explicitly selected
+// by their names and IPv6 sockets are opened on these interfaces.
+TEST_F(CfgIfaceTest, explicitNamesV6) {
+ CfgIface cfg;
+ // Specify valid interface names. There should be no error.
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "eth0"));
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "eth1"));
+
+ // Open sockets on specified interfaces.
+ cfg.openSockets(AF_INET6, DHCP6_SERVER_PORT);
+
+ // Sockets should be now open on eth0 and eth1, but not on loopback.
+ EXPECT_TRUE(socketOpen("eth0", AF_INET6));
+ EXPECT_TRUE(socketOpen("eth1", AF_INET6));
+ EXPECT_FALSE(socketOpen("lo", AF_INET6));
+
+ // No IPv4 sockets should be present because we wanted IPv6 sockets.
+ EXPECT_FALSE(socketOpen("eth0", AF_INET));
+ EXPECT_FALSE(socketOpen("eth1", AF_INET));
+ EXPECT_FALSE(socketOpen("lo", AF_INET));
+
+ // Close all sockets and make sure they are really closed.
+ cfg.closeSockets();
+ ASSERT_FALSE(socketOpen("eth0", AF_INET6));
+ ASSERT_FALSE(socketOpen("eth1", AF_INET6));
+ ASSERT_FALSE(socketOpen("lo", AF_INET6));
+
+ // Reset configuration and select only one interface this time.
+ cfg.reset();
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "eth1"));
+
+ cfg.openSockets(AF_INET6, DHCP6_SERVER_PORT);
+
+ // Socket should be open on eth1 only.
+ EXPECT_FALSE(socketOpen("eth0", AF_INET6));
+ EXPECT_TRUE(socketOpen("eth1", AF_INET6));
+ EXPECT_FALSE(socketOpen("lo", AF_INET6));
+}
+
+// This test checks that the wildcard interface name can be specified to
+// select all interfaces to open IPv4 sockets.
+TEST_F(CfgIfaceTest, wildcardV4) {
+ CfgIface cfg;
+ ASSERT_NO_THROW(cfg.use(AF_INET, "*"));
+
+ cfg.openSockets(AF_INET, DHCP4_SERVER_PORT);
+
+ // Sockets should be now open on eth0 and eth1, but not on loopback.
+ EXPECT_TRUE(socketOpen("eth0", AF_INET));
+ EXPECT_TRUE(socketOpen("eth1", AF_INET));
+ EXPECT_FALSE(socketOpen("lo", AF_INET));
+
+ // No IPv6 sockets should be present because we wanted IPv4 sockets.
+ EXPECT_FALSE(socketOpen("eth0", AF_INET6));
+ EXPECT_FALSE(socketOpen("eth1", AF_INET6));
+ EXPECT_FALSE(socketOpen("lo", AF_INET6));
+}
+
+// This test checks that the wildcard interface name can be specified to
+// select all interfaces to open IPv6 sockets.
+TEST_F(CfgIfaceTest, wildcardV6) {
+ CfgIface cfg;
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "*"));
+
+ cfg.openSockets(AF_INET6, DHCP6_SERVER_PORT);
+
+ // Sockets should be now open on eth0 and eth1, but not on loopback.
+ EXPECT_TRUE(socketOpen("eth0", AF_INET6));
+ EXPECT_TRUE(socketOpen("eth1", AF_INET6));
+ EXPECT_FALSE(socketOpen("lo", AF_INET6));
+
+ // No IPv6 sockets should be present because we wanted IPv6 sockets.
+ EXPECT_FALSE(socketOpen("eth0", AF_INET));
+ EXPECT_FALSE(socketOpen("eth1", AF_INET));
+ EXPECT_FALSE(socketOpen("lo", AF_INET));
+}
+
+// Test that unicast address can be specified for the socket to be opened on
+// the interface on which the socket bound to link local address is also
+// opened.
+TEST_F(CfgIfaceTest, validUnicast) {
+ CfgIface cfg;
+
+ // One socket will be opened on link-local address, one on unicast but
+ // on the same interface.
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "eth0"));
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "eth0/2001:db8:1::1"));
+
+ cfg.openSockets(AF_INET6, DHCP6_SERVER_PORT);
+
+ EXPECT_TRUE(socketOpen("eth0", AF_INET6));
+ EXPECT_TRUE(unicastOpen("eth0"));
+}
+
+// Test that when invalid interface names are specified an exception is thrown.
+TEST_F(CfgIfaceTest, invalidValues) {
+ CfgIface cfg;
+ ASSERT_THROW(cfg.use(AF_INET, ""), InvalidIfaceName);
+ ASSERT_THROW(cfg.use(AF_INET, " "), InvalidIfaceName);
+ ASSERT_THROW(cfg.use(AF_INET, "bogus"), NoSuchIface);
+
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth0"));
+ ASSERT_THROW(cfg.use(AF_INET, "eth0"), DuplicateIfaceName);
+
+ ASSERT_THROW(cfg.use(AF_INET, "eth0/2001:db8:1::1"), InvalidIfaceName);
+
+ ASSERT_THROW(cfg.use(AF_INET6, "eth0/"), InvalidIfaceName);
+ ASSERT_THROW(cfg.use(AF_INET6, "/2001:db8:1::1"), InvalidIfaceName);
+ ASSERT_THROW(cfg.use(AF_INET6, "*/2001:db8:1::1"), InvalidIfaceName);
+ ASSERT_THROW(cfg.use(AF_INET6, "bogus/2001:db8:1::1"), NoSuchIface);
+ ASSERT_THROW(cfg.use(AF_INET6, "eth0/2001:db8:1::2"), NoSuchAddress);
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "*"));
+ ASSERT_THROW(cfg.use(AF_INET6, "*"), DuplicateIfaceName);
+}
+
+// This test checks that it is possible to specify the loopback interface.
+// Note that without a link-local address an unicast address is required.
+TEST_F(CfgIfaceTest, explicitLoopbackV6) {
+ CfgIface cfg;
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "lo/::1"));
+
+ // Open sockets on specified interfaces and addresses.
+ cfg.openSockets(AF_INET6, DHCP6_SERVER_PORT);
+
+ EXPECT_TRUE(socketOpen("lo", AF_INET6));
+
+ // Close all sockets and make sure they are really closed.
+ cfg.closeSockets();
+ ASSERT_FALSE(socketOpen("lo", AF_INET6));
+
+ // Reset configuration.
+ cfg.reset();
+
+ // Retry with wildcard
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "*"));
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "lo/::1"));
+ cfg.openSockets(AF_INET6, DHCP6_SERVER_PORT);
+ // The logic used to require lo to be used only on its own, not with a
+ // wildcard. That constraint was removed.
+ EXPECT_TRUE(socketOpen("lo", AF_INET6));
+ cfg.closeSockets();
+ ASSERT_FALSE(socketOpen("lo", AF_INET6));
+
+ // Retry with a second interface
+ cfg.reset();
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "eth0"));
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "lo/::1"));
+ cfg.openSockets(AF_INET6, DHCP6_SERVER_PORT);
+ // The logic used to require lo to be used only on its own, not with a
+ // wildcard. That constraint was removed.
+ EXPECT_TRUE(socketOpen("lo", AF_INET6));
+ cfg.closeSockets();
+ ASSERT_FALSE(socketOpen("lo", AF_INET6));
+
+ // Finally with interfaces and addresses
+ cfg.reset();
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "eth0/2001:db8:1::1"));
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "lo/::1"));
+ cfg.openSockets(AF_INET6, DHCP6_SERVER_PORT);
+ // The logic used to require lo to be used only on its own, not with a
+ // wildcard. That constraint was removed.
+ EXPECT_TRUE(socketOpen("lo", AF_INET6));
+ cfg.closeSockets();
+ ASSERT_FALSE(socketOpen("lo", AF_INET6));
+}
+
+// Test that the equality and inequality operators work fine for CfgIface.
+TEST_F(CfgIfaceTest, equality) {
+ CfgIface cfg1;
+ CfgIface cfg2;
+
+ // Initially objects must be equal.
+ EXPECT_TRUE(cfg1 == cfg2);
+ EXPECT_FALSE(cfg1 != cfg2);
+
+ // Differ by one interface.
+ cfg1.use(AF_INET, "eth0");
+ EXPECT_FALSE(cfg1 == cfg2);
+ EXPECT_TRUE(cfg1 != cfg2);
+
+ // Now interfaces should be equal.
+ cfg2.use(AF_INET, "eth0");
+ EXPECT_TRUE(cfg1 == cfg2);
+ EXPECT_FALSE(cfg1 != cfg2);
+
+ // Differ by unicast address.
+ cfg1.use(AF_INET6, "eth0/2001:db8:1::1");
+ EXPECT_FALSE(cfg1 == cfg2);
+ EXPECT_TRUE(cfg1 != cfg2);
+
+ // Differ by unicast address and one interface.
+ cfg2.use(AF_INET6, "eth1");
+ EXPECT_FALSE(cfg1 == cfg2);
+ EXPECT_TRUE(cfg1 != cfg2);
+
+ // Now, the unicast addresses are equal but still differ by one interface.
+ cfg2.use(AF_INET6, "eth0/2001:db8:1::1");
+ EXPECT_FALSE(cfg1 == cfg2);
+ EXPECT_TRUE(cfg1 != cfg2);
+
+ // They should be now back to equal.
+ cfg1.use(AF_INET6, "eth1");
+ EXPECT_TRUE(cfg1 == cfg2);
+ EXPECT_FALSE(cfg1 != cfg2);
+
+ // Even though the wildcard doesn't change anything because all interfaces
+ // are already in use, the fact that the wildcard is specified should
+ // cause them to be not equal.
+ cfg1.use(AF_INET6, "*");
+ EXPECT_FALSE(cfg1 == cfg2);
+ EXPECT_TRUE(cfg1 != cfg2);
+
+ // Finally, both are equal as they use wildcard.
+ cfg2.use(AF_INET, "*");
+ EXPECT_TRUE(cfg1 == cfg2);
+ EXPECT_FALSE(cfg1 != cfg2);
+
+ // Differ by socket type.
+ cfg1.useSocketType(AF_INET, "udp");
+ EXPECT_FALSE(cfg1 == cfg2);
+ EXPECT_TRUE(cfg1 != cfg2);
+
+ // Now, both should use the same socket type.
+ cfg2.useSocketType(AF_INET, "udp");
+ EXPECT_TRUE(cfg1 == cfg2);
+ EXPECT_FALSE(cfg1 != cfg2);
+}
+
+// This test verifies that it is possible to unparse the interface config.
+TEST_F(CfgIfaceTest, unparse) {
+ CfgIface cfg4;
+
+ // Add things in it
+ EXPECT_NO_THROW(cfg4.use(AF_INET, "*"));
+ EXPECT_NO_THROW(cfg4.use(AF_INET, "eth0"));
+ EXPECT_NO_THROW(cfg4.use(AF_INET, "eth1/192.0.2.3"));
+ std::string comment = "{ \"comment\": \"foo\", \"bar\": 1 }";
+ EXPECT_NO_THROW(cfg4.setContext(Element::fromJSON(comment)));
+
+ // Check unparse
+ std::string expected =
+ "{ "
+ "\"interfaces\": [ \"*\", \"eth0\", \"eth1/192.0.2.3\" ], "
+ "\"re-detect\": false, "
+ "\"user-context\": { \"comment\": \"foo\", \"bar\": 1 } }";
+ runToElementTest<CfgIface>(expected, cfg4);
+
+ // Now check IPv6
+ CfgIface cfg6;
+ EXPECT_NO_THROW(cfg6.use(AF_INET6, "*"));
+ EXPECT_NO_THROW(cfg6.use(AF_INET6, "eth1"));
+ EXPECT_NO_THROW(cfg6.use(AF_INET6, "eth0/2001:db8:1::1"));
+ comment = "{ \"comment\": \"bar\", \"foo\": 2 }";
+ EXPECT_NO_THROW(cfg6.setContext(Element::fromJSON(comment)));
+
+ expected =
+ "{ "
+ "\"interfaces\": [ \"*\", \"eth1\", \"eth0/2001:db8:1::1\" ], "
+ "\"re-detect\": false, "
+ "\"user-context\": { \"comment\": \"bar\", \"foo\": 2 } }";
+ 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;
+
+ // Configure a fail callback
+ uint16_t fail_calls = 0;
+ CfgIface::OpenSocketsFailedCallback on_fail_callback =
+ [&fail_calls](ReconnectCtlPtr reconnect_ctl) {
+ EXPECT_TRUE(reconnect_ctl);
+ EXPECT_TRUE(reconnect_ctl->exitOnFailure());
+ fail_calls++;
+ };
+
+ CfgIface::open_sockets_failed_callback_ = on_fail_callback;
+
+ 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);
+ cfg4.setServiceSocketsMaxRetries(0);
+ cfg6.setServiceSocketsRequireAll(true);
+ cfg6.setServiceSocketsMaxRetries(0);
+
+ // Open the available ports
+ 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 = [](uint16_t) {
+ 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_NO_THROW(cfg4.openSockets(AF_INET, DHCP4_SERVER_PORT));
+ EXPECT_NO_THROW(cfg6.openSockets(AF_INET6, DHCP6_SERVER_PORT));
+
+ // Both instances should call the fail callback.
+ EXPECT_EQ(fail_calls, 2);
+}
+
+// This test verifies that if any IPv4 socket fails to bind,
+// the opening will retry.
+TEST_F(CfgIfaceTest, retryOpenServiceSockets4) {
+ CfgIface cfg4;
+
+ ASSERT_NO_THROW(cfg4.use(AF_INET, "eth0"));
+ ASSERT_NO_THROW(cfg4.use(AF_INET, "eth1/192.0.2.3"));
+
+ // Parameters
+ const uint16_t RETRIES = 5;
+ const uint16_t WAIT_TIME = 10; // miliseconds
+ // The number of sockets opened in a single retry attempt.
+ // 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
+ const uint16_t CALLS_PER_RETRY = 2;
+
+ // Require retry socket binding
+ cfg4.setServiceSocketsMaxRetries(RETRIES);
+ cfg4.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, WAIT_TIME](uint16_t) {
+ auto now = std::chrono::system_clock::now();
+
+ // Check waiting time only for the first call in a retry attempt.
+ if (total_calls % CALLS_PER_RETRY == 0) {
+ // Don't check the waiting time for initial call.
+ if (total_calls != 0) {
+ auto interval = now - last_call_time;
+ auto interval_ms =
+ std::chrono::duration_cast<std::chrono::milliseconds>(
+ interval
+ ).count();
+
+ EXPECT_GE(interval_ms, 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()
+ );
+
+ filter->setOpenSocketCallback(open_callback);
+
+ ASSERT_TRUE(filter);
+ ASSERT_NO_THROW(IfaceMgr::instance().setPacketFilter(filter));
+
+ // Open an unavailable port
+ ASSERT_NO_THROW(cfg4.openSockets(AF_INET, DHCP4_SERVER_PORT));
+
+ // Wait for a finish sockets binding (with a safe margin).
+ doWait(RETRIES * WAIT_TIME * 2);
+
+ // For each interface perform 1 init open and a few retries.
+ EXPECT_EQ(CALLS_PER_RETRY * (RETRIES + 1), 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, retryOpenServiceSockets4OmitBound) {
+ CfgIface cfg4;
+
+ ASSERT_NO_THROW(cfg4.use(AF_INET, "eth0"));
+ ASSERT_NO_THROW(cfg4.use(AF_INET, "eth1/192.0.2.3"));
+
+ // Parameters
+ const uint16_t RETRIES = 5;
+ const uint16_t WAIT_TIME = 10; // miliseconds
+
+ // Require retry socket binding
+ cfg4.setServiceSocketsMaxRetries(RETRIES);
+ cfg4.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, 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
+ if (total_calls > 1) {
+ auto interval = now - last_call_time;
+ auto interval_ms =
+ std::chrono::duration_cast<std::chrono::milliseconds>(
+ interval
+ ).count();
+
+ EXPECT_GE(interval_ms, WAIT_TIME);
+ }
+
+ last_call_time = now;
+
+ total_calls++;
+
+ // Fail to open a socket on eth0, success for eth1
+ if (!is_eth1) {
+ isc_throw(Unexpected, "CfgIfaceTest: cannot open a port");
+ }
+ };
+
+ boost::shared_ptr<isc::dhcp::test::PktFilterTestStub> filter(
+ new isc::dhcp::test::PktFilterTestStub()
+ );
+
+ filter->setOpenSocketCallback(open_callback);
+
+ ASSERT_TRUE(filter);
+ ASSERT_NO_THROW(IfaceMgr::instance().setPacketFilter(filter));
+
+ // Open an unavailable port
+ ASSERT_NO_THROW(cfg4.openSockets(AF_INET, DHCP4_SERVER_PORT));
+
+ // Wait for a finish sockets binding (with a safe margin).
+ doWait(RETRIES * WAIT_TIME * 2);
+
+ // For eth0 interface perform 1 init open and a few retries,
+ // for eth1 interface perform only init open.
+ EXPECT_EQ((RETRIES + 1) + 1, total_calls);
+}
+
+// Test that only one reopen timer is active simultaneously. If a new opening
+// starts, then the previous should be interrupted.
+TEST_F(CfgIfaceTest, retryDoubleOpenServiceSockets4) {
+ CfgIface cfg4;
+
+ ASSERT_NO_THROW(cfg4.use(AF_INET, "eth0"));
+
+ // Initial timer has a high frequency.
+ cfg4.setServiceSocketsMaxRetries(10000);
+ cfg4.setServiceSocketsRetryWaitTime(1);
+
+ // Set the callback that interrupt the previous execution.
+ uint16_t first_port_calls = 0;
+ uint16_t second_port_calls = 0;
+ auto open_callback = [&first_port_calls, &second_port_calls](uint16_t port) {
+ // First timer must be interrupted.
+ if (second_port_calls > 0) {
+ EXPECT_TRUE(port == 2);
+ }
+
+ if (port == 1) {
+ first_port_calls++;
+ } else {
+ second_port_calls++;
+ }
+
+ // Fail to open and retry.
+ isc_throw(Unexpected, "CfgIfaceTest: cannot open a port");
+ };
+
+ boost::shared_ptr<isc::dhcp::test::PktFilterTestStub> filter(
+ new isc::dhcp::test::PktFilterTestStub()
+ );
+
+ filter->setOpenSocketCallback(open_callback);
+
+ ASSERT_TRUE(filter);
+ ASSERT_NO_THROW(IfaceMgr::instance().setPacketFilter(filter));
+
+ // First opening.
+ ASSERT_NO_THROW(cfg4.openSockets(AF_INET, 1));
+
+ // Wait a short time.
+ doWait(10);
+
+ // Reconfigure the interface parameters.
+ cfg4.setServiceSocketsMaxRetries(1);
+ cfg4.setServiceSocketsRetryWaitTime(10);
+
+ // Second opening.
+ ASSERT_NO_THROW(cfg4.openSockets(AF_INET, 2));
+
+ doWait(50);
+
+ // The first timer should perform some calls.
+ EXPECT_GT(first_port_calls, 0);
+ // The secondary timer should make 2 calls: initial and 1 retry.
+ EXPECT_EQ(second_port_calls, 2);
+}
+
+// This test verifies that if any IPv6 socket fails to bind,
+// the opening will retry.
+TEST_F(CfgIfaceTest, retryOpenServiceSockets6) {
+ 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 = 10; // miliseconds
+ // The number of sockets opened in a single retry attempt.
+ // 1 unicast and 2 multicast sockets.
+ // iface: eth0 addr: 2001:db8:1::1 port: 547 multicast: 0
+ // iface: eth0 addr: fe80::3a60:77ff:fed5:cdef port: 547 multicast: 1
+ // iface: eth1 addr: fe80::3a60:77ff:fed5:abcd port: 547 multicast: 1
+ const uint16_t CALLS_PER_RETRY = 3;
+
+ // Require retry socket binding
+ 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, WAIT_TIME](uint16_t) {
+ auto now = std::chrono::system_clock::now();
+
+ // Check waiting time only for the first call in a retry attempt.
+ if (total_calls % CALLS_PER_RETRY == 0) {
+ // Don't check the waiting time for initial call.
+ if (total_calls != 0) {
+ auto interval = now - last_call_time;
+ auto interval_ms =
+ std::chrono::duration_cast<std::chrono::milliseconds>(
+ interval
+ ).count();
+
+ EXPECT_GE(interval_ms, 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::PktFilter6TestStub> filter(
+ new isc::dhcp::test::PktFilter6TestStub()
+ );
+ filter->setOpenSocketCallback(open_callback);
+ ASSERT_TRUE(filter);
+ ASSERT_NO_THROW(IfaceMgr::instance().setPacketFilter(filter));
+
+ // Open an unavailable port
+ ASSERT_NO_THROW(cfg6.openSockets(AF_INET6, DHCP6_SERVER_PORT));
+
+ // Wait for a finish sockets binding (with a safe margin).
+ doWait(RETRIES * WAIT_TIME * 2);
+
+ // For each interface perform 1 init open and a few retries.
+ EXPECT_EQ(CALLS_PER_RETRY * (RETRIES + 1), 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, retryOpenServiceSockets6OmitBound) {
+ 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 = 10; // miliseconds
+
+ // Require retry socket binding
+ cfg6.setServiceSocketsMaxRetries(RETRIES);
+ cfg6.setServiceSocketsRetryWaitTime(WAIT_TIME);
+
+#if defined OS_LINUX
+ const uint32_t opened_by_eth0 = 3;
+#else
+ const uint32_t opened_by_eth0 = 2;
+#endif
+
+ // 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, WAIT_TIME](uint16_t) {
+ auto now = std::chrono::system_clock::now();
+ bool is_eth0 = total_calls < opened_by_eth0;
+
+ // 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
+ // iface: eth0 addr: ff02::1:2 port: 547 multicast: 0 --- only on Linux systems
+ // iface: eth1 addr: fe80::3a60:77ff:fed5:abcd port: 547 multicast: 1 - fails
+
+ if (total_calls > (opened_by_eth0 + 1)) {
+ auto interval = now - last_call_time;
+ auto interval_ms =
+ std::chrono::duration_cast<std::chrono::milliseconds>(
+ 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<isc::dhcp::test::PktFilter6TestStub> filter(
+ new isc::dhcp::test::PktFilter6TestStub()
+ );
+
+ filter->setOpenSocketCallback(open_callback);
+
+ ASSERT_TRUE(filter);
+ ASSERT_NO_THROW(IfaceMgr::instance().setPacketFilter(filter));
+
+ // Open an unavailable port
+ ASSERT_NO_THROW(cfg6.openSockets(AF_INET6, DHCP6_SERVER_PORT));
+
+ // Wait for a finish sockets binding (with a safe margin).
+ doWait(RETRIES * WAIT_TIME * 2);
+
+ // For eth0 interface perform only 3 (on Linux Systems or 2 otherwise) init open,
+ // for eth1 interface perform 1 init open and a few retries.
+ EXPECT_EQ((RETRIES + 1) + opened_by_eth0, total_calls);
+}
+
+// Test that only one reopen timer is active simultaneously. If a new opening
+// starts, then the previous should be interrupted.
+TEST_F(CfgIfaceTest, retryDoubleOpenServiceSockets6) {
+ CfgIface cfg6;
+
+ ASSERT_NO_THROW(cfg6.use(AF_INET6, "eth0"));
+
+ // Initial timer has a high frequency.
+ cfg6.setServiceSocketsMaxRetries(10000);
+ cfg6.setServiceSocketsRetryWaitTime(1);
+
+ // Set the callback that interrupt the previous execution.
+ uint16_t first_port_calls = 0;
+ uint16_t second_port_calls = 0;
+ auto open_callback = [&first_port_calls, &second_port_calls](uint16_t port) {
+ // First timer must be interrupted.
+ if (second_port_calls > 0) {
+ EXPECT_TRUE(port == 2);
+ }
+
+ if (port == 1) {
+ first_port_calls++;
+ } else {
+ second_port_calls++;
+ }
+
+ // Fail to open and retry.
+ isc_throw(Unexpected, "CfgIfaceTest: cannot open a port");
+ };
+
+ boost::shared_ptr<isc::dhcp::test::PktFilter6TestStub> filter(
+ new isc::dhcp::test::PktFilter6TestStub()
+ );
+
+ filter->setOpenSocketCallback(open_callback);
+
+ ASSERT_TRUE(filter);
+ ASSERT_NO_THROW(IfaceMgr::instance().setPacketFilter(filter));
+
+ // First opening.
+ ASSERT_NO_THROW(cfg6.openSockets(AF_INET6, 1));
+
+ // Wait a short time.
+ doWait(10);
+
+ // Reconfigure the interface parameters.
+ cfg6.setServiceSocketsMaxRetries(1);
+ cfg6.setServiceSocketsRetryWaitTime(10);
+
+ // Second opening.
+ ASSERT_NO_THROW(cfg6.openSockets(AF_INET6, 2));
+
+ doWait(50);
+
+ // The first timer should perform some calls.
+ EXPECT_GT(first_port_calls, 0);
+ // The secondary timer should make 2 calls: initial and 1 retry.
+ EXPECT_EQ(second_port_calls, 2);
+}
+
+// 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
+// direct traffic is only supported on those systems.
+#if defined OS_LINUX || defined OS_BSD
+TEST(CfgIfaceNoStubTest, useSocketType) {
+ CfgIface cfg;
+ // Select datagram sockets.
+ ASSERT_NO_THROW(cfg.useSocketType(AF_INET, "udp"));
+ EXPECT_EQ("udp", cfg.socketTypeToText());
+ ASSERT_NO_THROW(cfg.openSockets(AF_INET, 10067, true));
+ // For datagram sockets, the direct traffic is not supported.
+ ASSERT_TRUE(!IfaceMgr::instance().isDirectResponseSupported());
+
+ // Check unparse
+ std::string expected = "{\n"
+ " \"interfaces\": [ ],\n"
+ " \"dhcp-socket-type\": \"udp\",\n"
+ " \"re-detect\": false }";
+ runToElementTest<CfgIface>(expected, cfg);
+
+ // Select raw sockets.
+ ASSERT_NO_THROW(cfg.useSocketType(AF_INET, "raw"));
+ EXPECT_EQ("raw", cfg.socketTypeToText());
+ ASSERT_NO_THROW(cfg.openSockets(AF_INET, 10067, true));
+ // For raw sockets, the direct traffic is supported.
+ ASSERT_TRUE(IfaceMgr::instance().isDirectResponseSupported());
+
+ ASSERT_NO_THROW(cfg.useSocketType(AF_INET, CfgIface::SOCKET_UDP));
+ EXPECT_EQ("udp", cfg.socketTypeToText());
+ ASSERT_NO_THROW(cfg.openSockets(AF_INET, 10067, true));
+ ASSERT_TRUE(!IfaceMgr::instance().isDirectResponseSupported());
+
+ ASSERT_NO_THROW(cfg.useSocketType(AF_INET, CfgIface::SOCKET_RAW));
+ EXPECT_EQ("raw", cfg.socketTypeToText());
+ ASSERT_NO_THROW(cfg.openSockets(AF_INET, 10067, true));
+ ASSERT_TRUE(IfaceMgr::instance().isDirectResponseSupported());
+
+ // Test invalid values.
+ EXPECT_THROW(cfg.useSocketType(AF_INET, "default"),
+ InvalidSocketType);
+ EXPECT_THROW(cfg.useSocketType(AF_INET6, "udp"),
+ InvalidSocketType);
+}
+#endif
+
+} // end of anonymous namespace