// Copyright (C) 2016-2024 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 #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace isc::asiolink; namespace ph = std::placeholders; namespace { /// @brief Local server address used for testing. const char SERVER_ADDRESS[] = "127.0.0.1"; /// @brief Local server port used for testing. const unsigned short SERVER_PORT = 18123; /// @brief Test timeout in ms. const long TEST_TIMEOUT = 10000; /// @brief Simple class representing TCP socket callback. class SocketCallback { public: /// @brief Implements callback for the asynchronous operation on the socket. /// /// This callback merely checks if error has occurred and reports this /// error. It does nothing in case of success. /// /// @param ec Error code. /// @param length Length of received data. void operator()(boost::system::error_code ec, size_t length = 0) { if (ec) { ADD_FAILURE() << "error occurred for a socket with data of length " << length << ": " << ec.message(); } } }; /// @brief Entity which can connect to the TCP server endpoint and close the /// connection. class TCPClient : public boost::noncopyable { public: /// @brief Constructor. /// /// This constructor creates new socket instance. It doesn't connect. Call /// connect() to connect to the server. /// /// @param io_service IO service to be stopped on error. explicit TCPClient(const IOServicePtr& io_service, bool& running) : io_service_(io_service), socket_(io_service_->getInternalIOService()), running_(running) { } /// @brief Destructor. /// /// Closes the underlying socket if it is open. ~TCPClient() { close(); } /// @brief Connect to the test server address and port. /// /// This method asynchronously connects to the server endpoint and uses the /// connectHandler as a callback function. void connect() { boost::asio::ip::tcp::endpoint endpoint(boost::asio::ip::address::from_string(SERVER_ADDRESS), SERVER_PORT); socket_.async_connect(endpoint, std::bind(&TCPClient::connectHandler, this, ph::_1)); } /// @brief Callback function for connect(). /// /// This function stops the IO service upon error. /// /// @param ec Error code. void connectHandler(const boost::system::error_code& ec) { if (!running_) { return; } if (ec) { // One would expect that async_connect wouldn't return EINPROGRESS // error code, but simply wait for the connection to get // established before the handler is invoked. It turns out, however, // that on some OSes the connect handler may receive this error code // which doesn't necessarily indicate a problem. Making an attempt // to write and read from this socket will typically succeed. So, // we ignore this error. if (ec.value() != boost::asio::error::in_progress) { ADD_FAILURE() << "error occurred while connecting: " << ec.message(); io_service_->stop(); } } } /// @brief Close connection. void close() { socket_.close(); } private: /// @brief Holds the IO service. IOServicePtr io_service_; /// @brief A socket used for the connection. boost::asio::ip::tcp::socket socket_; /// @brief Flag which indicates if the test is still running. bool& running_; }; /// @brief Pointer to the TCPClient. typedef boost::shared_ptr TCPClientPtr; /// @brief A signature of the function implementing callback for the /// TCPAcceptor. typedef std::function TCPAcceptorCallback; /// @brief TCPAcceptor using TCPAcceptorCallback. typedef TCPAcceptor TestTCPAcceptor; /// @brief Implements asynchronous TCP acceptor service. /// /// It creates a new socket into which connection is accepted. The socket /// is retained until class instance exists. class Acceptor { public: /// @brief Constructor. /// /// @param io_service IO service. /// @param acceptor Reference to the TCP acceptor on which asyncAccept /// will be called. /// @param callback Callback function for the asyncAccept. explicit Acceptor(const IOServicePtr& io_service, TestTCPAcceptor& acceptor, const TCPAcceptorCallback& callback) : socket_(io_service), acceptor_(acceptor), callback_(callback) { } /// @brief Destructor. /// /// Closes socket. ~Acceptor() { socket_.close(); } /// @brief Asynchronous accept new connection. void accept() { acceptor_.asyncAccept(socket_, callback_); } /// @brief Close connection. void close() { socket_.close(); } private: /// @brief Socket into which connection is accepted. TCPSocket socket_; /// @brief Reference to the TCPAcceptor on which asyncAccept is called. TestTCPAcceptor& acceptor_; /// @brief Instance of the callback used for asyncAccept. TCPAcceptorCallback callback_; }; /// @brief Pointer to the Acceptor object. typedef boost::shared_ptr AcceptorPtr; /// @brief Test fixture class for TCPAcceptor. /// /// This class provides means for creating new TCP connections, i.e. simulates /// clients connecting to the servers via TCPAcceptor. It is possible to create /// multiple simultaneous connections, which are retained by the test fixture /// class and closed cleanly when the test fixture is destroyed. class TCPAcceptorTest : public ::testing::Test { public: /// @brief Constructor. /// /// Besides initializing class members it also sets the test timer to guard /// against endlessly running IO service when TCP connections are /// unsuccessful. TCPAcceptorTest() : io_service_(new IOService()), acceptor_(io_service_), asio_endpoint_(boost::asio::ip::address::from_string(SERVER_ADDRESS), SERVER_PORT), endpoint_(asio_endpoint_), test_timer_(io_service_), connections_(), clients_(), connections_num_(0), aborted_connections_num_(0), max_connections_(1), running_(true) { test_timer_.setup(std::bind(&TCPAcceptorTest::timeoutHandler, this), TEST_TIMEOUT, IntervalTimer::ONE_SHOT); } /// @brief Destructor. virtual ~TCPAcceptorTest() { running_ = false; test_timer_.cancel(); io_service_->restart(); try { io_service_->poll(); } catch (...) { } } /// @brief Specifies how many new connections are expected before the IO /// service is stopped. /// /// @param max_connections Connections limit. void setMaxConnections(const unsigned int max_connections) { max_connections_ = max_connections; } /// @brief Create ASIO endpoint from the provided endpoint by retaining the /// IP address and modifying the port. /// /// This convenience method is useful to create new endpoint from the /// existing endpoint to test reusing IP address for multiple acceptors. /// The returned endpoint has the same IP address but different port. /// /// @param endpoint Source endpoint. /// /// @return New endpoint with the port number increased by 1. boost::asio::ip::tcp::endpoint createSiblingEndpoint(const boost::asio::ip::tcp::endpoint& endpoint) const { boost::asio::ip::tcp::endpoint endpoint_copy(endpoint); endpoint_copy.port(endpoint.port() + 1); return (endpoint_copy); } /// @brief Opens TCP acceptor and sets 'reuse address' option. void acceptorOpen() { acceptor_.open(endpoint_); acceptor_.setOption(TestTCPAcceptor::ReuseAddress(true)); } /// @brief Starts accepting TCP connections. /// /// This method creates new Acceptor instance and calls accept() to start /// accepting new connections. The instance of the Acceptor object is /// retained in the connections_ list. void accept() { TCPAcceptorCallback cb = std::bind(&TCPAcceptorTest::acceptHandler, this, ph::_1); AcceptorPtr conn(new Acceptor(io_service_, acceptor_, cb)); connections_.push_back(conn); connections_.back()->accept(); } /// @brief Connect to the endpoint. /// /// This method creates TCPClient instance and retains it in the clients_ /// list. void connect() { TCPClientPtr client(new TCPClient(io_service_, running_)); clients_.push_back(client); clients_.back()->connect(); } /// @brief Callback function for asynchronous accept calls. /// /// It stops the IO service upon error or when the number of accepted /// connections reaches the max_connections_ value. Otherwise it calls /// accept() to start accepting next connections. /// /// @param ec Error code. void acceptHandler(const boost::system::error_code& ec) { if (ec) { if (ec.value() != boost::asio::error::operation_aborted) { ADD_FAILURE() << "error occurred while accepting connection: " << ec.message(); } else { ++aborted_connections_num_; } io_service_->stop(); } // We have reached the maximum number of connections - end the test. if (++connections_num_ >= max_connections_) { io_service_->stop(); return; } accept(); } /// @brief Callback function invoke upon test timeout. /// /// It stops the IO service and reports test timeout. void timeoutHandler() { ADD_FAILURE() << "Timeout occurred while running the test!"; io_service_->stop(); } /// @brief IO service. IOServicePtr io_service_; /// @brief TCPAcceptor under test. TestTCPAcceptor acceptor_; /// @brief Server endpoint. boost::asio::ip::tcp::endpoint asio_endpoint_; /// @brief asiolink server endpoint (uses asio_endpoint_). TCPEndpoint endpoint_; /// @brief Asynchronous timer service to detect timeouts. IntervalTimer test_timer_; /// @brief List of connections on the server side. std::list connections_; /// @brief List of client connections. std::list clients_; /// @brief Current number of established connections. unsigned int connections_num_; /// @brief Current number of aborted connections. unsigned int aborted_connections_num_; /// @brief Connections limit. unsigned int max_connections_; /// @brief Flag which indicates if the test is still running. bool running_; }; // Test TCPAcceptor::asyncAccept. TEST_F(TCPAcceptorTest, asyncAccept) { // Establish up to 10 connections. setMaxConnections(10); // Initialize acceptor. acceptorOpen(); acceptor_.bind(endpoint_); acceptor_.listen(); // Start accepting new connections. accept(); // Create 10 new TCP connections (client side). for (unsigned int i = 0; i < 10; ++i) { connect(); } // Run the IO service until we have accepted 10 connections, an error // or test timeout occurred. io_service_->run(); // Make sure that all accepted connections have been recorded. EXPECT_EQ(10, connections_num_); EXPECT_EQ(10, connections_.size()); } // Check that it is possible to set SO_REUSEADDR flag for the TCPAcceptor. TEST_F(TCPAcceptorTest, reuseAddress) { // We need at least two acceptors using common address. Let's create the // second endpoint which has the same address but different port. boost::asio::ip::tcp::endpoint asio_endpoint2(createSiblingEndpoint(asio_endpoint_)); TCPEndpoint endpoint2(asio_endpoint2); // Create and open two acceptors. TestTCPAcceptor acceptor1(io_service_); TestTCPAcceptor acceptor2(io_service_); ASSERT_NO_THROW(acceptor1.open(endpoint_)); ASSERT_NO_THROW(acceptor2.open(endpoint2)); // Set SO_REUSEADDR socket option so as acceptors can bind to the /// same address. ASSERT_NO_THROW( acceptor1.setOption(TestTCPAcceptor::ReuseAddress(true)) ); ASSERT_NO_THROW( acceptor2.setOption(TestTCPAcceptor::ReuseAddress(true)) ); ASSERT_NO_THROW(acceptor1.bind(endpoint_)); ASSERT_NO_THROW(acceptor2.bind(endpoint2)); // Create third acceptor, but don't set the SO_REUSEADDR. It should // refuse to bind. TCPEndpoint endpoint3(createSiblingEndpoint(asio_endpoint2)); TestTCPAcceptor acceptor3(io_service_); ASSERT_NO_THROW(acceptor3.open(endpoint3)); EXPECT_THROW(acceptor3.bind(endpoint_), boost::system::system_error); } // Test that TCPAcceptor::getProtocol returns IPPROTO_TCP. TEST_F(TCPAcceptorTest, getProtocol) { EXPECT_EQ(IPPROTO_TCP, acceptor_.getProtocol()); } // Test that TCPAcceptor::getNative returns valid socket descriptor. TEST_F(TCPAcceptorTest, getNative) { // Initially the descriptor should be invalid (negative). ASSERT_LT(acceptor_.getNative(), 0); // Now open the socket and make sure the returned descriptor is now valid. ASSERT_NO_THROW(acceptorOpen()); EXPECT_GE(acceptor_.getNative(), 0); } // macOS 10.12.3 has a bug which causes the connections to not enter // the TIME-WAIT state and they never get closed. #if !defined (OS_OSX) // Test that TCPAcceptor::close works properly. TEST_F(TCPAcceptorTest, close) { // Initialize acceptor. acceptorOpen(); acceptor_.bind(endpoint_); acceptor_.listen(); // Start accepting new connections. accept(); // Create 10 new TCP connections (client side). for (unsigned int i = 0; i < 10; ++i) { connect(); } // Close the acceptor before connections are accepted. acceptor_.close(); // Run the IO service. io_service_->run(); // The connections should have been aborted. EXPECT_EQ(1, connections_num_); EXPECT_EQ(1, aborted_connections_num_); EXPECT_EQ(1, connections_.size()); } #endif }