Browse Source

[master] Merge branch 'trac5094'

Marcin Siodelski 8 years ago
parent
commit
920ba90696

+ 1 - 0
src/bin/dhcp4/ctrl_dhcp4_srv.h

@@ -7,6 +7,7 @@
 #ifndef CTRL_DHCPV4_SRV_H
 #define CTRL_DHCPV4_SRV_H
 
+#include <asiolink/asio_wrapper.h>
 #include <asiolink/asiolink.h>
 #include <cc/data.h>
 #include <cc/command_interpreter.h>

+ 1 - 0
src/bin/dhcp6/ctrl_dhcp6_srv.h

@@ -7,6 +7,7 @@
 #ifndef CTRL_DHCPV6_SRV_H
 #define CTRL_DHCPV6_SRV_H
 
+#include <asiolink/asio_wrapper.h>
 #include <asiolink/asiolink.h>
 #include <cc/data.h>
 #include <cc/command_interpreter.h>

+ 1 - 0
src/bin/dhcp6/kea_controller.cc

@@ -6,6 +6,7 @@
 
 #include <config.h>
 
+#include <asiolink/asio_wrapper.h>
 #include <asiolink/asiolink.h>
 #include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/parsers/dhcp_config_parser.h>

+ 1 - 0
src/lib/asiolink/Makefile.am

@@ -27,6 +27,7 @@ libkea_asiolink_la_SOURCES += io_endpoint.cc io_endpoint.h
 libkea_asiolink_la_SOURCES += io_error.h
 libkea_asiolink_la_SOURCES += io_service.h io_service.cc
 libkea_asiolink_la_SOURCES += io_socket.h io_socket.cc
+libkea_asiolink_la_SOURCES += tcp_acceptor.h
 libkea_asiolink_la_SOURCES += tcp_endpoint.h
 libkea_asiolink_la_SOURCES += tcp_socket.h
 libkea_asiolink_la_SOURCES += udp_endpoint.h

+ 1 - 0
src/lib/asiolink/io_asio_socket.h

@@ -78,6 +78,7 @@ class IOEndpoint;
 
 template <typename C>
 class IOAsioSocket : public IOSocket {
+
     ///
     /// \name Constructors and Destructor
     ///

+ 11 - 1
src/lib/asiolink/io_socket.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2010-2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2010-2017 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
@@ -35,6 +35,16 @@ namespace asiolink {
 /// derived class for testing purposes rather than providing factory methods
 /// (i.e., getDummy variants below).
 class IOSocket {
+public:
+
+    /// @name Types of objects encapsulating socket options.
+    //@{
+
+    /// @brief Represents SO_REUSEADDR socket option.
+    typedef boost::asio::socket_base::reuse_address ReuseAddress;
+
+    //@}
+
     ///
     /// \name Constructors and Destructor
     ///

+ 134 - 0
src/lib/asiolink/tcp_acceptor.h

@@ -0,0 +1,134 @@
+// Copyright (C) 2016-2017 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/.
+
+#ifndef TCP_ACCEPTOR_H
+#define TCP_ACCEPTOR_H
+
+#ifndef BOOST_ASIO_HPP
+#error "asio.hpp must be included before including this, see asiolink.h as to why"
+#endif
+
+#include <asiolink/io_service.h>
+#include <asiolink/io_socket.h>
+#include <asiolink/tcp_endpoint.h>
+#include <asiolink/tcp_socket.h>
+#include <boost/shared_ptr.hpp>
+#include <netinet/in.h>
+
+namespace isc {
+namespace asiolink {
+
+/// @brief Provides a service for accepting new TCP connections.
+///
+/// Internally it uses @c boost::asio::ip::tcp::acceptor class to implement
+/// the acceptor service.
+///
+/// @tparam C Acceptor callback type.
+template<typename C>
+class TCPAcceptor : public IOSocket {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// @param io_service IO service.
+    explicit TCPAcceptor(IOService& io_service)
+        : IOSocket(),
+          acceptor_(new boost::asio::ip::tcp::acceptor(io_service.get_io_service())) {
+    }
+
+    /// @brief Destructor.
+    virtual ~TCPAcceptor() { }
+
+    /// @brief Returns file descriptor of the underlying socket.
+    virtual int getNative() const final {
+        return (acceptor_->native());
+    }
+
+    /// @brief Returns protocol of the socket.
+    ///
+    /// @return IPPROTO_TCP.
+    virtual int getProtocol() const final {
+        return (IPPROTO_TCP);
+    }
+
+    /// @brief Opens acceptor socket given the endpoint.
+    ///
+    /// @param endpoint Reference to the endpoint object which specifies the
+    /// address and port on which the acceptor service will run.
+    void open(const TCPEndpoint& endpoint) {
+        acceptor_->open(endpoint.getASIOEndpoint().protocol());
+    }
+
+    /// @brief Sets socket option.
+    ///
+    /// Typically, this method is used to set SO_REUSEADDR option on the socket:
+    /// @code
+    /// IOService io_service;
+    /// TCPAcceptor<Callback> acceptor(io_service);
+    /// acceptor.setOption(TCPAcceptor::ReuseAddress(true))
+    /// @endcode
+    ///
+    /// @param socket_option Reference to the object encapsulating an option to
+    /// be set for the socket.
+    /// @tparam SettableSocketOption Type of the object encapsulating socket option
+    /// being set.
+    template<typename SettableSocketOption>
+    void setOption(const SettableSocketOption& socket_option) {
+        acceptor_->set_option(socket_option);
+    }
+
+    /// @brief Binds socket to an endpoint.
+    ///
+    /// @param endpoint Reference to an endpoint to which the socket is to
+    /// be bound.
+    void bind(const TCPEndpoint& endpoint) {
+        acceptor_->bind(endpoint.getASIOEndpoint());
+    }
+
+    /// @brief Starts listening for the new connections.
+    void listen() {
+        acceptor_->listen();
+    }
+
+    /// @brief Asynchronously accept new connection.
+    ///
+    /// This method accepts new connection into the specified socket. When the
+    /// new connection arrives or an error occurs the specified callback function
+    /// is invoked.
+    ///
+    /// @param socket Socket into which connection should be accepted.
+    /// @param callback Callback function to be invoked when the new connection
+    /// arrives.
+    /// @tparam SocketCallback Type of the callback for the @ref TCPSocket.
+    template<typename SocketCallback>
+    void asyncAccept(const TCPSocket<SocketCallback>& socket, C& callback) {
+        acceptor_->async_accept(socket.getASIOSocket(), callback);
+    }
+
+    /// @brief Checks if the acceptor is open.
+    ///
+    /// @return true if acceptor is open.
+    bool isOpen() const {
+        return (acceptor_->is_open());
+    }
+
+    /// @brief Closes the acceptor.
+    void close() const {
+        acceptor_->close();
+    }
+
+private:
+
+    /// @brief Underlying ASIO acceptor implementation.
+    boost::shared_ptr<boost::asio::ip::tcp::acceptor> acceptor_;
+
+};
+
+
+} // namespace asiolink
+} // namespace isc
+
+#endif

+ 7 - 1
src/lib/asiolink/tcp_socket.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2016 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
@@ -153,6 +153,12 @@ public:
     /// \brief Close socket
     virtual void close();
 
+    /// \brief Returns reference to the underlying ASIO socket.
+    ///
+    /// \return Reference to underlying ASIO socket.
+    virtual boost::asio::ip::tcp::socket& getASIOSocket() const {
+        return (socket_);
+    }
 
 private:
     // Two variables to hold the socket - a socket and a pointer to it.  This

+ 1 - 0
src/lib/asiolink/tests/Makefile.am

@@ -27,6 +27,7 @@ run_unittests_SOURCES += udp_endpoint_unittest.cc
 run_unittests_SOURCES += udp_socket_unittest.cc
 run_unittests_SOURCES += io_service_unittest.cc
 run_unittests_SOURCES += dummy_io_callback_unittest.cc
+run_unittests_SOURCES += tcp_acceptor_unittest.cc
 
 run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
 

+ 422 - 0
src/lib/asiolink/tests/tcp_acceptor_unittest.cc

@@ -0,0 +1,422 @@
+// Copyright (C) 2016-2017 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 <asiolink/asio_wrapper.h>
+#include <asiolink/interval_timer.h>
+#include <asiolink/io_address.h>
+#include <asiolink/io_service.h>
+#include <asiolink/tcp_acceptor.h>
+#include <asiolink/tcp_endpoint.h>
+#include <boost/bind.hpp>
+#include <boost/function.hpp>
+#include <boost/noncopyable.hpp>
+#include <boost/shared_ptr.hpp>
+#include <gtest/gtest.h>
+#include <list>
+#include <netinet/in.h>
+#include <string>
+
+using namespace isc::asiolink;
+
+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: " << 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(IOService& io_service)
+        : io_service_(io_service.get_io_service()), socket_(io_service_) {
+    }
+
+    /// @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,
+                              boost::bind(&TCPClient::connectHandler, this,_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 (ec) {
+            ADD_FAILURE() << "error occurred while connecting: "
+                          << ec.message();
+            io_service_.stop();
+        }
+    }
+
+    /// @brief Close connection.
+    void close() {
+        socket_.close();
+    }
+
+private:
+
+    /// @brief Holds reference to the IO service.
+    boost::asio::io_service& io_service_;
+
+    /// @brief A socket used for the connection.
+    boost::asio::ip::tcp::socket socket_;
+
+};
+
+/// @brief Pointer to the TCPClient.
+typedef boost::shared_ptr<TCPClient> TCPClientPtr;
+
+/// @brief A signature of the function implementing callback for the
+/// TCPAcceptor.
+typedef boost::function<void(const boost::system::error_code&)> TCPAcceptorCallback;
+
+/// @brief TCPAcceptor using TCPAcceptorCallback.
+typedef TCPAcceptor<TCPAcceptorCallback> 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(IOService& 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<SocketCallback> 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<Acceptor> 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_(), 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) {
+        test_timer_.setup(boost::bind(&TCPAcceptorTest::timeoutHandler, this),
+                                      TEST_TIMEOUT, IntervalTimer::ONE_SHOT);
+    }
+
+    /// @brief Destructor.
+    virtual ~TCPAcceptorTest() {
+    }
+
+    /// @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 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 = boost::bind(&TCPAcceptorTest::acceptHandler,
+                                             this, _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_));
+        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.
+    IOService 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<AcceptorPtr> connections_;
+
+    /// @brief List of client connections.
+    std::list<TCPClientPtr> 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_;
+};
+
+// Test TCPAcceptor::asyncAccept.
+TEST_F(TCPAcceptorTest, asyncAccept) {
+    // Establish up to 10 connections.
+    setMaxConnections(10);
+
+    // Initialize acceptor.
+    acceptor_.open(endpoint_);
+    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(acceptor_.open(endpoint_));
+    EXPECT_GE(acceptor_.getNative(), 0);
+}
+
+
+// Test that TCPAcceptor::close works properly.
+TEST_F(TCPAcceptorTest, close) {
+    // Initialize acceptor.
+    acceptor_.open(endpoint_);
+    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());
+}
+
+}