summaryrefslogtreecommitdiffstats
path: root/src/lib/asiolink/tls_socket.h
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/asiolink/tls_socket.h')
-rw-r--r--src/lib/asiolink/tls_socket.h519
1 files changed, 519 insertions, 0 deletions
diff --git a/src/lib/asiolink/tls_socket.h b/src/lib/asiolink/tls_socket.h
new file mode 100644
index 0000000..cdd2f78
--- /dev/null
+++ b/src/lib/asiolink/tls_socket.h
@@ -0,0 +1,519 @@
+// Copyright (C) 2021 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 TLS_SOCKET_H
+#define TLS_SOCKET_H
+
+#ifndef BOOST_ASIO_HPP
+#error "asio.hpp must be included before including this, see asiolink.h as to why"
+#endif
+
+#include <asiolink/crypto_tls.h>
+#include <asiolink/tcp_socket.h>
+
+#include <boost/noncopyable.hpp>
+
+namespace isc {
+namespace asiolink {
+
+/// @brief The @c TLSSocket class is a concrete derived class of @c IOAsioSocket
+/// that represents a TLS socket.
+///
+/// @tparam C Callback type.
+template <typename C>
+class TLSSocket : public IOAsioSocket<C>, private boost::noncopyable {
+public:
+
+ /// @brief Constructor from a TLS stream.
+ ///
+ /// It is assumed that the caller will open and close the stream,
+ /// so these operations are a no-op for that stream.
+ ///
+ /// @param stream The TLS stream.
+ TLSSocket(TlsStream<C>& stream);
+
+ /// @brief Constructor.
+ ///
+ /// Used when the TLSSocket is being asked to manage its own internal
+ /// socket. In this case, the open() and close() methods are used.
+ ///
+ /// @param service I/O Service object used to manage the socket.
+ /// @param context Pointer to TLS context.
+ TLSSocket(IOService& service, TlsContextPtr context);
+
+ /// @brief Destructor.
+ virtual ~TLSSocket() { }
+
+ /// @brief Return file descriptor of underlying socket.
+ virtual int getNative() const {
+#if BOOST_VERSION < 106600
+ return (socket_.native());
+#else
+ return (socket_.native_handle());
+#endif
+ }
+
+ /// @brief Return protocol of socket.
+ virtual int getProtocol() const {
+ return (IPPROTO_TCP);
+ }
+
+ /// @brief Is "open()" synchronous predicate.
+ ///
+ /// Indicates that the opening of a TLS socket is asynchronous.
+ virtual bool isOpenSynchronous() const {
+ return (false);
+ }
+
+ /// @brief Checks if the connection is usable.
+ ///
+ /// The connection is usable if the socket is open and the peer has not
+ /// closed its connection.
+ ///
+ /// @return true if the connection is usable.
+ bool isUsable() const {
+ // If the socket is open it doesn't mean that it is still
+ // usable. The connection could have been closed on the other
+ // end. We have to check if we can still use this socket.
+ if (socket_.is_open()) {
+ // Remember the current non blocking setting.
+ const bool non_blocking_orig = socket_.non_blocking();
+
+ // Set the socket to non blocking mode. We're going to
+ // test if the socket returns would_block status on the
+ // attempt to read from it.
+ socket_.non_blocking(true);
+
+ // Use receive with message peek flag to avoid removing
+ // the data awaiting to be read.
+ char data[2];
+ int err = 0;
+ int cc = recv(getNative(), data, sizeof(data), MSG_PEEK);
+ if (cc < 0) {
+ // Error case.
+ err = errno;
+ } else if (cc == 0) {
+ // End of file.
+ err = -1;
+ }
+
+ // Revert the original non_blocking flag on the socket.
+ socket_.non_blocking(non_blocking_orig);
+
+ // If the connection is alive we'd typically get
+ // would_block status code. If there are any data that
+ // haven't been read we may also get success status. We're
+ // guessing that try_again may also be returned by some
+ // implementations in some situations. Any other error
+ // code indicates a problem with the connection so we
+ // assume that the connection has been closed.
+ return ((err == 0) || (err == EAGAIN) || (err == EWOULDBLOCK));
+ }
+
+ return (false);
+ }
+
+ /// @brief Open Socket.
+ ///
+ /// Opens the TLS socket. This is an asynchronous operation, completion of
+ /// which will be signalled via a call to the callback function.
+ ///
+ /// @param endpoint Endpoint to which the socket will connect.
+ /// @param callback Callback object.
+ virtual void open(const IOEndpoint* endpoint, C& callback);
+
+ /// @brief Perform Handshake.
+ ///
+ /// Perform the TLS handshake. This is an asynchronous operation,
+ /// completion of which will be signalled via a call to the callback
+ /// function.
+ ///
+ /// @param callback Callback object.
+ virtual void handshake(C& callback);
+
+ /// @brief Send Asynchronously.
+ ///
+ /// Calls the underlying socket's async_send() method to send a
+ /// packet of data asynchronously to the remote endpoint. The
+ /// callback will be called on completion.
+ ///
+ /// @param data Data to send.
+ /// @param length Length of data to send.
+ /// @param endpoint Target of the send. (Unused for a TLS socket because
+ /// that was determined when the connection was opened.)
+ /// @param callback Callback object.
+ /// @throw BufferTooLarge on attempt to send a buffer larger than 64kB.
+ virtual void asyncSend(const void* data, size_t length,
+ const IOEndpoint* endpoint, C& callback);
+
+ /// @brief Send Asynchronously without count.
+ ///
+ /// This variant of the method sends data over the TLS socket without
+ /// preceding the data with a data count. Eventually, we should migrate
+ /// the virtual method to not insert the count but there are existing
+ /// classes using the count. Once this migration is done, the existing
+ /// virtual method should be replaced by this method.
+ ///
+ /// @param data Data to send.
+ /// @param length Length of data to send.
+ /// @param callback Callback object.
+ /// @throw BufferTooLarge on attempt to send a buffer larger than 64kB.
+ void asyncSend(const void* data, size_t length, C& callback);
+
+ /// @brief Receive Asynchronously.
+ ///
+ /// Calls the underlying socket's async_receive() method to read a packet
+ /// of data from a remote endpoint. Arrival of the data is signalled via a
+ /// call to the callback function.
+ ///
+ /// @param data Buffer to receive incoming message.
+ /// @param length Length of the data buffer.
+ /// @param offset Offset into buffer where data is to be put.
+ /// @param endpoint Source of the communication.
+ /// @param callback Callback object.
+ virtual void asyncReceive(void* data, size_t length, size_t offset,
+ IOEndpoint* endpoint, C& callback);
+
+ /// @brief Process received data packet.
+ ///
+ /// See the description of IOAsioSocket::receiveComplete for a complete
+ /// description of this method.
+ ///
+ /// @param staging Pointer to the start of the staging buffer.
+ /// @param length Amount of data in the staging buffer.
+ /// @param cumulative Amount of data received before the staging buffer is
+ /// processed.
+ /// @param offset Unused.
+ /// @param expected unused.
+ /// @param outbuff Output buffer. Data in the staging buffer is be copied
+ /// to this output buffer in the call.
+ ///
+ /// @return Always true.
+ virtual bool processReceivedData(const void* staging, size_t length,
+ size_t& cumulative, size_t& offset,
+ size_t& expected,
+ isc::util::OutputBufferPtr& outbuff);
+
+ /// @brief Cancel I/O On Socket.
+ virtual void cancel();
+
+ /// @brief Close socket.
+ virtual void close();
+
+ /// @brief TLS shutdown.
+ ///
+ /// The callback is called on completion i.e. when the peer performs
+ /// a shutdown or a close.
+ virtual void shutdown(C& callback);
+
+ /// @brief Returns reference to the underlying ASIO socket.
+ ///
+ /// @return Reference to underlying ASIO socket.
+ virtual typename TlsStream<C>::lowest_layer_type& getASIOSocket() const {
+ return (socket_);
+ }
+
+ /// @brief Returns reference to the underlying TLS stream.
+ ///
+ /// @return Reference to underlying TLS stream.
+ virtual TlsStream<C>& getTlsStream() const {
+ return (stream_);
+ }
+
+private:
+ /// Two variables to hold the stream - a stream and a pointer to it. This
+ /// handles the case where a stream is passed to the TLSSocket on
+ /// construction, or where it is asked to manage its own stream.
+
+ /// @brief Pointer to own stream.
+ std::unique_ptr<TlsStream<C>> stream_ptr_;
+
+ /// @brief TLS stream.
+ TlsStream<C>& stream_;
+
+ /// @brief Underlying TCP socket.
+ typename TlsStream<C>::lowest_layer_type& socket_;
+
+ /// @todo Remove temporary buffer
+ /// The current implementation copies the buffer passed to asyncSend() into
+ /// a temporary buffer and precedes it with a two-byte count field. As
+ /// ASIO should really be just about sending and receiving data, the TCP
+ /// code should not do this. If the protocol using this requires a two-byte
+ /// count, it should add it before calling this code. (This may be best
+ /// achieved by altering isc::dns::buffer to have pairs of methods:
+ /// getLength()/getTCPLength(), getData()/getTCPData(), with the getTCPXxx()
+ /// methods taking into account a two-byte count field.)
+ ///
+ /// The option of sending the data in two operations, the count followed by
+ /// the data was discounted as that would lead to two callbacks which would
+ /// cause problems with the stackless coroutine code.
+
+ /// @brief Send buffer.
+ isc::util::OutputBufferPtr send_buffer_;
+};
+
+// Constructor - caller manages socket.
+
+template <typename C>
+TLSSocket<C>::TLSSocket(TlsStream<C>& stream) :
+ stream_ptr_(), stream_(stream),
+ socket_(stream_.lowest_layer()), send_buffer_() {
+}
+
+// Constructor - create socket on the fly.
+
+template <typename C>
+TLSSocket<C>::TLSSocket(IOService& service, TlsContextPtr context) :
+ stream_ptr_(new TlsStream<C>(service, context)),
+ stream_(*stream_ptr_), socket_(stream_.lowest_layer()), send_buffer_()
+{
+}
+
+// Open the socket.
+
+template <typename C> void
+TLSSocket<C>::open(const IOEndpoint* endpoint, C& callback) {
+ // Ignore opens on already-open socket. Don't throw a failure because
+ // of uncertainties as to what precedes when using asynchronous I/O.
+ // Also allows us a treat a passed-in socket as a self-managed socket.
+ if (!socket_.is_open()) {
+ if (endpoint->getFamily() == AF_INET) {
+ socket_.open(boost::asio::ip::tcp::v4());
+ } else {
+ socket_.open(boost::asio::ip::tcp::v6());
+ }
+
+ // Set options on the socket:
+
+ // Reuse address - allow the socket to bind to a port even if the port
+ // is in the TIMED_WAIT state.
+ socket_.set_option(boost::asio::socket_base::reuse_address(true));
+ }
+
+ // Upconvert to a TCPEndpoint. We need to do this because although
+ // IOEndpoint is the base class of UDPEndpoint and TCPEndpoint, it does not
+ // contain a method for getting at the underlying endpoint type - that is in
+ /// the derived class and the two classes differ on return type.
+ isc_throw_assert(endpoint->getProtocol() == IPPROTO_TCP);
+ const TCPEndpoint* tcp_endpoint =
+ static_cast<const TCPEndpoint*>(endpoint);
+
+ // Connect to the remote endpoint. On success, the handler will be
+ // called (with one argument - the length argument will default to
+ // zero).
+ socket_.async_connect(tcp_endpoint->getASIOEndpoint(), callback);
+}
+
+// Perform the handshake.
+
+template <typename C> void
+TLSSocket<C>::handshake(C& callback) {
+ if (!socket_.is_open()) {
+ isc_throw(SocketNotOpen, "attempt to perform handshake on "
+ "a TLS socket that is not open");
+ }
+ stream_.handshake(callback);
+}
+
+// Send a message. Should never do this if the socket is not open, so throw
+// an exception if this is the case.
+
+template <typename C> void
+TLSSocket<C>::asyncSend(const void* data, size_t length, C& callback)
+{
+ if (!socket_.is_open()) {
+ isc_throw(SocketNotOpen,
+ "attempt to send on a TLS socket that is not open");
+ }
+
+ try {
+ send_buffer_.reset(new isc::util::OutputBuffer(length));
+ send_buffer_->writeData(data, length);
+
+ // Send the data.
+ boost::asio::async_write(stream_,
+ boost::asio::buffer(send_buffer_->getData(),
+ send_buffer_->getLength()),
+ callback);
+ } catch (const boost::numeric::bad_numeric_cast&) {
+ isc_throw(BufferTooLarge,
+ "attempt to send buffer larger than 64kB");
+ }
+}
+
+template <typename C> void
+TLSSocket<C>::asyncSend(const void* data, size_t length,
+ const IOEndpoint*, C& callback)
+{
+ if (!socket_.is_open()) {
+ isc_throw(SocketNotOpen,
+ "attempt to send on a TLS socket that is not open");
+ }
+
+ /// Need to copy the data into a temporary buffer and precede it with
+ /// a two-byte count field.
+ /// @todo arrange for the buffer passed to be preceded by the count
+ try {
+ // Ensure it fits into 16 bits
+ uint16_t count = boost::numeric_cast<uint16_t>(length);
+
+ // Copy data into a buffer preceded by the count field.
+ send_buffer_.reset(new isc::util::OutputBuffer(length + 2));
+ send_buffer_->writeUint16(count);
+ send_buffer_->writeData(data, length);
+
+ // ... and send it
+ boost::asio::async_write(stream_,
+ boost::asio::buffer(send_buffer_->getData(),
+ send_buffer_->getLength()),
+ callback);
+ } catch (const boost::numeric::bad_numeric_cast&) {
+ isc_throw(BufferTooLarge,
+ "attempt to send buffer larger than 64kB");
+ }
+}
+
+// Receive a message. Note that the "offset" argument is used as an index
+// into the buffer in order to decide where to put the data. It is up to the
+// caller to initialize the data to zero
+template <typename C> void
+TLSSocket<C>::asyncReceive(void* data, size_t length, size_t offset,
+ IOEndpoint* endpoint, C& callback)
+{
+ if (!socket_.is_open()) {
+ isc_throw(SocketNotOpen,
+ "attempt to receive from a TLS socket that is not open");
+ }
+
+ // Upconvert to a TCPEndpoint. We need to do this because although
+ // IOEndpoint is the base class of UDPEndpoint and TCPEndpoint, it
+ // does not contain a method for getting at the underlying endpoint
+ // type - that is in the derived class and the two classes differ on
+ // return type.
+ isc_throw_assert(endpoint->getProtocol() == IPPROTO_TCP);
+ TCPEndpoint* tcp_endpoint = static_cast<TCPEndpoint*>(endpoint);
+
+ // Write the endpoint details from the communications link. Ideally
+ // we should make IOEndpoint assignable, but this runs in to all sorts
+ // of problems concerning the management of the underlying Boost
+ // endpoint (e.g. if it is not self-managed, is the copied one
+ // self-managed?) The most pragmatic solution is to let Boost take care
+ // of everything and copy details of the underlying endpoint.
+ tcp_endpoint->getASIOEndpoint() = socket_.remote_endpoint();
+
+ // Ensure we can write into the buffer and if so, set the pointer to
+ // where the data will be written.
+ if (offset >= length) {
+ isc_throw(BufferOverflow, "attempt to read into area beyond end of "
+ "TCP receive buffer");
+ }
+ void* buffer_start =
+ static_cast<void*>(static_cast<uint8_t*>(data) + offset);
+
+ // ... and kick off the read.
+ stream_.async_read_some(boost::asio::buffer(buffer_start, length - offset),
+ callback);
+}
+
+// Is the receive complete?
+
+template <typename C> bool
+TLSSocket<C>::processReceivedData(const void* staging, size_t length,
+ size_t& cumulative, size_t& offset,
+ size_t& expected,
+ isc::util::OutputBufferPtr& outbuff)
+{
+ // Point to the data in the staging buffer and note how much there is.
+ const uint8_t* data = static_cast<const uint8_t*>(staging);
+ size_t data_length = length;
+
+ // Is the number is "expected" valid? It won't be unless we have received
+ // at least two bytes of data in total for this set of receives.
+ if (cumulative < 2) {
+
+ // "expected" is not valid. Did this read give us enough data to
+ // work it out?
+ cumulative += length;
+ if (cumulative < 2) {
+
+ // Nope, still not valid. This must have been the first packet and
+ // was only one byte long. Tell the fetch code to read the next
+ // packet into the staging buffer beyond the data that is already
+ // there so that the next time we are called we have a complete
+ // TCP count.
+ offset = cumulative;
+ return (false);
+ }
+
+ // Have enough data to interpret the packet count, so do so now.
+ expected = isc::util::readUint16(data, cumulative);
+
+ // We have two bytes less of data to process. Point to the start of the
+ // data and adjust the packet size. Note that at this point,
+ // "cumulative" is the true amount of data in the staging buffer, not
+ // "length".
+ data += 2;
+ data_length = cumulative - 2;
+ } else {
+
+ // Update total amount of data received.
+ cumulative += length;
+ }
+
+ // Regardless of anything else, the next read goes into the start of the
+ // staging buffer.
+ offset = 0;
+
+ // Work out how much data we still have to put in the output buffer. (This
+ // could be zero if we have just interpreted the TCP count and that was
+ // set to zero.)
+ if (expected >= outbuff->getLength()) {
+
+ // Still need data in the output packet. Copy what we can from the
+ // staging buffer to the output buffer.
+ size_t copy_amount = std::min(expected - outbuff->getLength(),
+ data_length);
+ outbuff->writeData(data, copy_amount);
+ }
+
+ // We can now say if we have all the data.
+ return (expected == outbuff->getLength());
+}
+
+// Cancel I/O on the socket. No-op if the socket is not open.
+
+template <typename C> void
+TLSSocket<C>::cancel() {
+ if (socket_.is_open()) {
+ socket_.cancel();
+ }
+}
+
+// TLS shutdown. Can be used for orderly close.
+
+template <typename C> void
+TLSSocket<C>::shutdown(C& callback) {
+ if (!socket_.is_open()) {
+ isc_throw(SocketNotOpen, "attempt to perform shutdown on "
+ "a TLS socket that is not open");
+ }
+ stream_.shutdown(callback);
+}
+
+// Close the socket down. Can only do this if the socket is open and we are
+// managing it ourself.
+
+template <typename C> void
+TLSSocket<C>::close() {
+ if (socket_.is_open() && stream_ptr_) {
+ socket_.close();
+ }
+}
+
+} // namespace asiolink
+} // namespace isc
+
+#endif // TLS_SOCKET_H