diff --git a/CHANGELOG.md b/CHANGELOG.md
index 66f1fa7b4459..9f184b80a0dc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
+## [7.0.13]
+
+[7.0.13]: https://github.com/microsoft/CCF/releases/tag/ccf-7.0.13
+
+### Changed
+
+- TLS is now terminated by OpenSSL directly on the socket, rather than being relayed over the ringbuffer and decrypted through a memory BIO. The session interfaces in `include/ccf/node/session.h` and `include/ccf/research/custom_protocol_subsystem_interface.h` have changed shape accordingly: a session now receives and emits plaintext, taking ownership of the inbound buffer, and writes its output through a `ccf::SessionWriter` rather than a `tls::Context` (#8117).
+
## [7.0.12]
[7.0.12]: https://github.com/microsoft/CCF/releases/tag/ccf-7.0.12
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e136af2d2920..f362ae88662b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -706,10 +706,10 @@ if(BUILD_TESTS)
)
add_unit_test(
- rpc_connections_test
- ${CMAKE_CURRENT_SOURCE_DIR}/src/host/test/rpc_connections.cpp
+ openssl_server_test
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/host/test/openssl_server_test.cpp
)
- target_link_libraries(rpc_connections_test PRIVATE uv)
+ target_link_libraries(openssl_server_test PRIVATE ccf_tasks http_parser uv)
add_unit_test(
raft_test
@@ -870,7 +870,10 @@ if(BUILD_TESTS)
${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/node_info_json.cpp
)
- add_unit_test(tls_test ${CMAKE_CURRENT_SOURCE_DIR}/src/tls/test/main.cpp)
+ add_unit_test(
+ tls_test
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/clients/tls/test/main.cpp
+ )
target_link_libraries(tls_test PRIVATE ${CMAKE_THREAD_LIBS_INIT})
add_unit_test(
diff --git a/doc/architecture/tls_internals.rst b/doc/architecture/tls_internals.rst
index e7acbd4802af..2353a2237512 100644
--- a/doc/architecture/tls_internals.rst
+++ b/doc/architecture/tls_internals.rst
@@ -4,106 +4,156 @@ TLS Internals
Overview
~~~~~~~~
-In CCF, the :term:`TLS` layer is implemented using OpenSSL 3.3. However, the original implementation was using OE's MbedTLS library, which the current implementation replaced. During the transition period, the OpenSSL implementation had to emulate the previous MbedTLS one and the remaining code isn't particularly suited to OpenSSL.
+In CCF, the :term:`TLS` layer is implemented using OpenSSL (3.3 or later).
-This document describes how that works, to facilitate further changes.
+TLS is terminated in the **connection layer**: OpenSSL owns the socket file descriptor directly. The existing host ``libuv`` loop reports file descriptor readiness, and a per-connection worker drives the non-blocking handshake, reads, writes and graceful close. Everything above the connection layer, including HTTP parsing and endpoint dispatch, only ever sees plaintext.
-Enclave Connections
-~~~~~~~~~~~~~~~~~~~
+This document describes the connection layer and its interface to the session layer above it.
-CCF handles RPC requests by managing a collection of HTTPS sessions on each node. The HTTP session receives raw TCP bytes from the :term:`ring buffer` and passes it to the TLS implementation that tries to decrypt on read (after a successful handshake).
+Layers
+~~~~~~
-If there isn't enough information to decrypt on read, the TLS layer responds with a 'WANTS_READ' message, meaning more packets are needed to complete the message, so the endpoint tried to get more data from the ring buffer.
+A single RPC interface is served by these pieces:
-Once all incoming data is decrypted, the HTTP session parses the plain text data and passes the parsed request to the application, which process it and produces a plain text response. The session then serialises this response, encrypts it through the TLS layer, and writes the encrypted data to the :term:`ring buffer` to be sent to the original caller.
+- :ccf_repo:`OpenSSLServer ` owns the listening and accepted sockets and registers ``uv_poll_t`` handles for them on the existing host loop. Each accepted connection holds an ``SSL`` object bound to its file descriptor with ``SSL_set_fd``. It emits decrypted bytes through an ``OnData`` callback and reports teardown through ``OnClose``.
+- :ccf_repo:`OpenSSLSessionManager ` bridges the transport to the session layer. It lazily creates one ``ccf::Session`` per connection using a caller-supplied factory, and implements :ccf_repo:`ccf::SessionWriter ` so that a session's outbound plaintext is handed back to the transport.
+- :ccf_repo:`ccf::PlaintextSession ` is the base for the protocol sessions (:ccf_repo:`HTTPServerSession `, :ccf_repo:`HTTP2ServerSession `). It receives plaintext, and emits plaintext through its ``SessionWriter``.
+- :ccf_repo:`RPCConnectionManager ` owns one of these stacks per configured RPC interface, and holds the cross-interface policy: certificates, session caps, and metrics.
-TLS Implementation
-~~~~~~~~~~~~~~~~~~
+Because TLS lives below the session, there is no separate "encrypted session" type. The difference between a TLS interface and an ``UNSECURED`` one is a flag on the connection layer, not a different session class.
-The TLS implementation in 'src/tls' has three main components:
+Note that the inbound and outbound paths are not symmetric. Inbound plaintext is pushed straight into the session, but a session cannot touch the socket: it hands bytes to a ``SessionWriter``, which queues them for the owning connection. This is what allows sessions to run on any worker thread while each socket is still only ever touched by one thread at a time.
-- A 'Certificate Authority' (CA), which has a root certificate that can sign other certificates in the network. This CA certificate is generated by the network outside of the TLS implementation and is imported when clients and servers are created.
-- A 'Certificate' (Cert), which is the server/client's own certificate, signed by the CA and unique to this server or client.
-- A 'Context', which is the common implementation of both 'Client' and 'Server' and provides input (read and decrypt) and output (encrypt and write) over the ring buffer and its triggers. Clients and servers are basically just a Context with a certificate.
+Listening
+~~~~~~~~~
-Both CA and Cert have internal logic to validate their certificates and private keys, and the Context has logic to complete a TLS handshake, read and write using encryption and to query the peer's certificate.
+The listening socket is resolved with ``getaddrinfo`` (rather than ``inet_pton``) so that interfaces can be configured with hostnames such as ``localhost`` and with IPv6 addresses, not only IPv4 literals. ``SO_REUSEADDR`` is set, so that a port left in ``TIME_WAIT`` by a previous process can be rebound on restart.
-The next layer up is the 'TLSSession' (of which 'HTTPSession' owns an instance) that holds the ring buffer, the pending read and write buffers, and the call backs for sending and receiving data.
+``SO_REUSEPORT`` is deliberately not set. There is one listener per interface, so it would buy nothing today, and it would weaken two useful properties: a second node misconfigured onto the same port would bind successfully instead of failing with ``EADDRINUSE``, with connections then split between the two at random; and any process running with the same effective UID could bind the same port and siphon off a share of inbound connections. If per-worker listeners are added later, it should be enabled explicitly for that case rather than unconditionally.
-Receiving Messages
-~~~~~~~~~~~~~~~~~~
+After binding, the actually bound port is read back with ``getsockname``. This supports configuring port ``0`` to request an ephemeral port, which the test infrastructure relies on, and is why resolved addresses are only known once the interface is listening.
-When a REST node message is received through the ring buffer, the session's ``handle_incoming_data_thread()`` method passes it to ``TLSSession::read()``, which in turn calls ``tls::Context::read()``.
+Certificates
+~~~~~~~~~~~~
-However, when creating the TLS 'Context', the 'TLSSession' had to register some callbacks of its own, too. This is one of the parts that was specific to MbedTLS and that was (unnaturally) replicated to OpenSSL.
+An interface starts listening before its certificate is necessarily known. A joining node, for example, only obtains the service certificate once its join request has been accepted. ``OpenSSLServer`` therefore accepts an empty certificate at construction, and refuses TLS connections until one is supplied.
-Those callbacks are implemented in 'TLSSession' because they need access to the ``pending_read`` buffers to read from and the 'ring buffer' to write to. But the TLS context ('SSL' and 'SSL_CTX' objects) need to encrypt/decrypt data, and they can only do that from 'BIO's in OpenSSL, so the callback has a mix of ring buffer and BIO handling that tricks both sides to take the appropriate steps at the right time in this complex dance.
+``set_server_cert()`` may be called from any thread. It does not build the ``SSL_CTX`` inline: it queues the request and wakes the loop, so the context is only ever created or replaced on the loop thread. This avoids locking the context on the hot path. Replacing the context affects only subsequent connections; existing connections keep the context they were created with, which OpenSSL keeps alive by reference counting.
-In the 'read' case, the message has arrived into the ``pending_read`` buffer via a (previously triggered) ring buffer callback, and that data is written to TLS's 'read' BIO (via ``BIO_write_ex``) `before` TLS itself reads it. That data is still encrypted, so when the TLS reads it with ``SSL_read_ex``, it tries to decrypt and on success, returns a plain text buffer. On error, it emits the error or a 'WANTS_READ' status, so the endpoint can try to extract more information from the ``pending_read`` buffer again.
+The server context sets a minimum version of TLS 1.2 and, when the interface is configured for HTTP/2, advertises ``h2`` via ALPN.
-Upon success, the read BIO is flushed and new data can be read again. The plain text result is returned to the HTTP session, which sends it to the application to process.
+Cryptographic policy
+~~~~~~~~~~~~~~~~~~~~
-Sending Messages
-~~~~~~~~~~~~~~~~
+The context restricts what the handshake may negotiate:
-When the application finishes, it returns a plain text response. The session passes that message to ``TLSSession::send_data()``, which fills the ``pending_write`` buffer and calls ``write_some()`` via ``flush()``. ``write_some()`` itself calls ``tls::Context::write()``.
+- TLS 1.2 is limited to four AES-GCM cipher suites, all with ECDHE key exchange and either ECDSA or RSA authentication.
+- TLS 1.3 is limited to ``TLS_AES_256_GCM_SHA384`` and ``TLS_AES_128_GCM_SHA256``.
+- The key exchange group list prefers the hybrid post-quantum groups ``SecP384r1MLKEM1024``, ``SecP256r1MLKEM768`` and ``X25519MLKEM768``, falling back to ``P-521``, ``P-384`` and ``P-256``. The hybrid groups are prefixed with ``?`` so that the list still loads on OpenSSL versions which do not implement them.
+- Renegotiation is disabled, and session resumption on renegotiation with it, to avoid the associated denial-of-service vectors.
+- Where a choice remains, the server's ordering wins over the client's.
-This is the same process as for reads: a callback in 'TLSSession' was registered for the 'write' BIO, with access to the ``pending_write`` and the ring buffer. But in this case, the callback is only executed `after` the TLS layer has encrypted the data and written to the 'write' BIO.
+Note that in TLS 1.3 the *client* effectively chooses the group: OpenSSL picks the first client-offered group that the server also supports, so the server list acts as a filter rather than a preference.
-Now, we take that (encrypted) data, flush the BIO ourselves (to avoid clogging the pipes with the next message) and send it through the ring buffer with the ``RINGBUFFER_TRY_WRITE_MESSAGE(::tcp::tcp_outbound, ...)`` macro.
+This policy is defined in one place, ``build_server_ctx()``. It is asserted from the wire by the ``openssl_server_test`` unit tests and, for a running service, by the :ccf_repo:`tls_groups ` end-to-end test. Both only check the hybrid groups when built with ``-DTEST_HYBRID_TLS_GROUPS=ON``, since those groups require OpenSSL 3.5 or later.
-This is what actually sends the message back to the client, so when this callback returns, the remaining stack just returns the status of that write. Different errors are treated at different levels (TLS errors in Context, ring buffer errors in TLSSession and HTTP errors in HTTPSession).
+The context also sets ``SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER`` and ``SSL_MODE_ENABLE_PARTIAL_WRITE``, both of which are required by the write path described below.
-Why OpenSSL?
-~~~~~~~~~~~~
+Client authentication
+~~~~~~~~~~~~~~~~~~~~~
+
+The server calls ``SSL_CTX_set_verify`` with ``SSL_VERIFY_PEER`` and an accept-all callback. This is deliberate: it makes the server *request* the client certificate during the handshake so that it is available for application-level caller authentication, without the TLS layer itself rejecting clients. Whether a certificate is required, and whether it is acceptable, is decided by the endpoint's authentication policy.
+
+Reading
+~~~~~~~
+
+When ``libuv`` reports a connection readable, the loop hands the connection to its worker (see `Threading`_), which calls ``SSL_read`` repeatedly until it reports ``SSL_ERROR_WANT_READ``. Every chunk of decrypted bytes is passed to the ``OnData`` callback as it is produced, so a single readable event may yield several callbacks.
+
+A single pass is capped at a fixed number of bytes so that one busy connection cannot monopolise its worker. Reaching that cap does not end the read: OpenSSL may still be holding buffered records, in which case the file descriptor is *not* readable and waiting for a further ``uv_poll`` event would stall the connection. The worker therefore reports ``SSL_has_pending()`` back to the loop, which immediately schedules another pass instead of re-arming ``UV_READABLE``. ``SSL_has_pending()`` rather than ``SSL_pending()``, because it also covers bytes which have been read off the socket but not yet processed into plaintext.
+
+``OpenSSLSessionManager`` receives those bytes, finds or creates the session for that connection, and calls ``handle_incoming_data``. The session dispatches the actual parsing to its own ``OrderedTasks``, so neither the loop thread nor the connection worker blocks on application work.
+
+``SSL_ERROR_WANT_WRITE`` on a read is not an error: a TLS 1.3 key update needs the socket to become writable, so the connection is left open with ``UV_WRITABLE`` armed. Any other result, whether a clean ``SSL_ERROR_ZERO_RETURN``, an unclean EOF, or a fatal error, closes the connection.
+
+Inbound flow control
+~~~~~~~~~~~~~~~~~~~~
+
+How much the node reads is bounded by how far behind the application is. ``OpenSSLSessionManager`` charges every chunk it hands to a session against a node-wide budget, and releases it once the session reports that chunk processed. Because request execution happens inside the parse task, "processed" means the request has actually run, so the budget measures unexecuted work rather than merely unparsed bytes.
+
+While the budget is exhausted, no interface arms ``UV_READABLE``. The sending client's own :term:`TCP` window then closes, and it cannot queue more work than the node can retire. Nothing is dropped or refused: reads simply pause and resume.
+
+The budget is node-wide rather than per-connection, so a single connection can exhaust it and pause the others. That is deliberate: a per-connection limit would have to be smaller than one maximum-sized request before it could bound total node memory to the same figure.
-The main reasons why we moved to OpenSSL are:
+Releasing the budget wakes every registered transport, not only the one whose session made room, so an interface with no traffic of its own does not stay paused. A session which never reports - a custom protocol, or one whose queued work is cancelled as it is destroyed - cannot strand the budget either: whatever is still outstanding for a connection is released when that connection is torn down.
-- We already use OpenSSL for our crypto library ('src/crypto').
-- We wanted TLS 1.3 support that the MbedTLS version in use did not provide.
-- We wanted to support QUIC through OpenSSL.
+A connection paused this way is performing no I/O, so it ages towards the idle timeout in the usual way. That is intentional: a connection which cannot make progress in either direction for the whole idle period is one the node is too far behind to serve.
-MbedTLS has since been removed from the runtime implementation.
+Writing and backpressure
+~~~~~~~~~~~~~~~~~~~~~~~~
-MbedTLS vs OpenSSL
-~~~~~~~~~~~~~~~~~~
+``send()`` is thread-safe. It appends the plaintext to a queue guarded by a mutex and signals a ``uv_async_t``, waking the loop thread, which moves the bytes onto the target connection and schedules a worker pass to attempt the write.
-As stated above, the current OpenSSL implementation is `emulating` the previous MbedTLS one, so some oddities are observed.
+Writes are where genuine backpressure appears. ``SSL_write`` on a non-blocking socket may report ``SSL_ERROR_WANT_WRITE`` after consuming only part of the buffer. The remainder stays buffered against the connection, ``UV_WRITABLE`` is armed, and the write resumes when the socket next becomes writable. Because the socket is owned by OpenSSL and is non-blocking, this reflects the real state of the :term:`TCP` send buffer rather than an internal approximation.
-First, MbedTLS returns errors as negative values and amount of data handled as positive values. OpenSSL concurs on positive values but returns 0 (or -1 in previous versions) for all errors, using ``SSL_get_error`` to then classify which error and what to do. The error values are also positive.
+Closing
+~~~~~~~
-To simulate this, we implement the error handling at each invocation and, on error, we negate the value of the error so that we can retain the old behavior of checking for negative values.
+Closing is deferred rather than immediate. A close requested while output is still buffered sets a flag and flushes; the file descriptor is only closed once the buffered bytes have been written.
-Second, MbedTLS keeps all its context (configuration, connection info, read and write buffers) in a single large structure, while OpenSSL has separate structures for each and uses 'BIO' objects for buffers. Reads and writes in MbedTLS is done exclusively via callbacks.
+This matters because the common pattern is to write a response and immediately close. Closing eagerly truncates any response large enough to have been backpressured, which the client observes as a connection reset partway through the body rather than as a well-formed response.
-OpenSSL callbacks, however, are very different from MbedTLS ones. They are called twice for each action, one before the actual action and another after.
+Server shutdown is the exception: there the close is forced rather than deferred. Each connection takes exactly one further worker pass, writing whatever the socket will accept, and then goes. Waiting for the flush would let a peer which has stopped reading hold the whole node's shutdown open indefinitely.
-To simulate this we had to implement a read callback `before` the BIO read (so we could fill it up with the contents of the ring buffer) and the write callback `after` the BIO write (so we could pick up its contents and send it into the ring buffer).
+Idle connections are closed separately. If an idle timeout is configured, a repeating ``uv_timer_t`` periodically sweeps connections whose last I/O is older than the timeout. This retains the once-per-second scheduling used by the previous RPC transport while comparing actual ``steady_clock`` timestamps rather than counting timer ticks.
-There is a complex dance of return values in OpenSSL's callbacks. If any returns errors the action is canceled immediately. On reads, because the BIO was empty, the initial return value is an error, so we must make sure that, if there is anything in the ``pending_read`` buffer, we have to change the status to the amount of bytes read, so it can continue.
+Threading
+~~~~~~~~~
-Third, the handshake in MbedTLS had various types of errors, which we had to emulate by making the appropriate ``SSL_*`` calls, check the peer certificate, etc. to get the same types of responses for the same situations.
+``OpenSSLServer`` does not create a thread or a private reactor. It splits its work between the existing host ``libuv`` thread and the general task system, along a single dividing line:
-Finally, in MbedTLS, the configuration and session objects were setup at the same time, while in OpenSSL they're separate. We ended up duplicating every single configuration, but this is unnecessary, because once the config object is correct, any session object created from it has the same properties.
+- The **loop thread** owns everything that touches the server's own state: accepting connections, the connection and identifier maps, ``uv_poll_t`` registration, building and replacing the ``SSL_CTX``, sweeping idle connections, and finally closing file descriptors. It performs no ``SSL`` operations.
+- Each connection owns an :ccf_repo:`OrderedTasks ` queue, and every ``SSL`` operation for that connection - ``SSL_new``, ``SSL_accept``, ``SSL_read``, ``SSL_write``, ``SSL_shutdown``, ``SSL_free`` - runs there, as does the ``OnData`` callback. This keeps handshakes and bulk encryption off the loop thread.
-But the TLS Context doesn't handle more than one session per configuration, so we could set either of them once and ignore the other. The simplest thing would be to setup just the session, but if we end up having more than one session later, we'd have to refactor that.
+Work reaches the loop in one of two ways: file descriptor readiness reported through ``uv_poll_t``, or a cross-thread request (a queued write, a close, or a certificate update) posted to a mutex-guarded queue and signalled through ``uv_async_t``. Either way the loop only ever *schedules* a pass over the connection; it never performs the I/O itself.
-Simplifying the OpenSSL Implementation
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+A connection is serviced by at most one worker pass at a time. Before dispatching, the loop calls ``uv_poll_stop`` for that connection, so no second pass can be scheduled while one is running. Readiness events and cross-thread commands that arrive meanwhile accumulate in loop-thread-only fields and are handed to the next pass. When a pass finishes it posts its result back to the loop, which re-arms polling, schedules another pass if more work accumulated, or tears the connection down.
-With MbedTLS gone from the code base, the OpenSSL implementation can be simplified.
+.. warning::
-The considerations are:
+ OpenSSL's error queue is **thread-local**, and successive passes over the same connection may run on different worker threads. ``SSL_get_error`` consults that queue, so an error left behind by unrelated work on the same thread can be misattributed to this connection.
-- We don't need to handle errors inside the calls to read/write, but can leave for each caller to handle IFF there is an error by calling ``SSL_get_error``. This also means we don't need to negate error values, as they're in different domains.
-- We can simplify the SSL configuration on startup, handshake handling and peer certificate handling.
+ Every ``SSL_accept``, ``SSL_read`` and ``SSL_write`` is therefore preceded by ``ERR_clear_error()``. Omitting this causes healthy keep-alive connections to be closed spuriously, because a stale error (for example a previous client disconnecting without ``close_notify``) is read as a fatal error on an unrelated connection.
-However, getting rid of the callbacks and using BIOs directly is going to be hard.
+Unsecured interfaces
+~~~~~~~~~~~~~~~~~~~~
-First, the current callback is in 'TLSSession' because it has access to both pending buffers and the ring buffer. The TLS Context does not have access to it nor it would be wise to pass references to it, as that'd make the Context exclusive to the TLSSession.
+An interface configured as ``UNSECURED`` runs the same loop with no ``SSL`` object. Connections are immediately ready, and reads and writes use ``::recv`` and ``::send`` directly. The buffering, backpressure, graceful close and idle handling are identical, so the session layer above is unchanged.
+
+Outbound connections
+~~~~~~~~~~~~~~~~~~~~
+
+``OpenSSLServer`` is inbound only. Outbound requests - fetching quote endorsements, refreshing JWT signing keys, and the recovery decision protocol - are made with libcurl, which does its own TLS.
+
+The only remaining OpenSSL client helpers, :ccf_repo:`CA ` and :ccf_repo:`Cert `, are used solely by the C++ test and perf clients in :ccf_repo:`src/clients `. No node code uses them.
+
+Why OpenSSL?
+~~~~~~~~~~~~
+
+CCF originally used MbedTLS, and the OpenSSL implementation that replaced it emulated MbedTLS conventions, notably negated error codes and a memory-BIO indirection driven by callbacks.
+
+Both are now gone. ``OpenSSLServer`` calls ``SSL_read``/``SSL_write`` on a socket-backed ``SSL`` and interprets ``SSL_get_error`` directly, and ``src/tls`` no longer exists.
+
+The reasons for OpenSSL remain:
+
+- OpenSSL is already used for the :doc:`cryptography ` implementation in ``src/crypto``.
+- TLS 1.3 support.
+- A path to QUIC.
+
+Future: QUIC
+~~~~~~~~~~~~
-Second, both endpoint and TLS have a need to read and write asynchronously. Data arrives from the ring buffer at any time and the TLS implementation can request reads and writes (for example, during handshake) that the endpoint didn't request itself.
+QUIC is not yet implemented. Server-side QUIC requires OpenSSL 3.5 or later, which adds ``SSL_new_listener``, ``SSL_accept_connection`` and ``OSSL_QUIC_server_method``; these are absent from the 3.3.x baseline CCF currently supports.
-So if ``SSL_handshake``, ``SSL_read_ex`` and ``SSL_write_ex`` don't have direct access to read and write from the ring buffers without direct requests from the endpoints, it won't be able to conclude the asynchronous handshake and start the connection.
+:ccf_repo:`DatagramServer ` exists as the substrate for that work. It is deliberately shaped as the UDP socket a QUIC server operates on: socket creation, binding, ``uv_poll_t`` readiness and per-datagram dispatch are all reusable as-is. The points that change for QUIC are marked ``QUIC EXTENSION POINT`` inline, and consist of wrapping the socket with ``BIO_new_dgram``/``SSL_set_fd`` on a listener ``SSL``, adding the OpenSSL event timeout, and replacing the datagram callback with ``SSL_handle_events``.
-One possible way out of it is to create a `BIO pair `_ for each read/write action between the 'TLSSession' and the TLS 'Context', driven by two asynchronous tasks in 'TLSSession' that just poll the BIOs and buffers and pass data across. This removes a callback, but introduces polling, which is not an actual improvement.
+Until then, an interface whose ``app_protocol`` is ``QUIC`` echoes each datagram straight back, statelessly. Other UDP interfaces are served by the custom protocol subsystem (:ccf_repo:`custom_protocol_subsystem_interface.h `) in the usual way, with one session per source address, swept on the same idle timeout as TCP connections.
diff --git a/doc/contribute/onboarding.rst b/doc/contribute/onboarding.rst
index 3cfc66930e34..49ee4cc9223c 100644
--- a/doc/contribute/onboarding.rst
+++ b/doc/contribute/onboarding.rst
@@ -63,12 +63,13 @@ Note that this diagram deliberately does not represent host-to-enclave communica
.. mermaid::
flowchart TB
- Client[HTTPS/1.1 Client auth] -- TLS 1.2 or 1.3 --> TLSSession
- TLSSession[TLS Session src] -- PlainText --> HTTPSession
+ Client[HTTPS/1.1 Client auth] -- TLS 1.2 or 1.3 --> TLSConnection
+ TLSConnection[TLS Connection src] -- PlainText --> HTTPSession
+ HTTPSession -- PlainText --> SessionWriter[Session Writer src]
+ SessionWriter --> TLSConnection
HTTPSession[HTTP Session src] -- Request --> Endpoint[Application Endpoint doc]
Endpoint -- Response --> HTTPSession
- HTTPSession --> TLSSession
- TLSSession --> Client
+ TLSConnection --> Client
Endpoint -- WriteSet --> Store[Store doc]
Store -- LedgerEntry --> Ledger[Ledger doc]
Ledger -- LedgerEntry --> Disk
diff --git a/include/ccf/node/session.h b/include/ccf/node/session.h
index da58f763a5ec..7f72a7b0d625 100644
--- a/include/ccf/node/session.h
+++ b/include/ccf/node/session.h
@@ -3,7 +3,7 @@
#pragma once
#include
-#include
+#include
namespace ccf
{
@@ -12,8 +12,55 @@ namespace ccf
public:
virtual ~Session() = default;
- virtual void handle_incoming_data(std::span data) = 0;
+ // Inbound bytes for this session. Ownership is transferred, so that the
+ // buffer the transport has already built is moved through the session
+ // rather than copied again.
+ virtual void handle_incoming_data(std::vector&& data) = 0;
virtual void send_data(std::vector&& data) = 0;
virtual void close_session() = 0;
};
+
+ // Abstract output sink injected into Sessions: a Session hands its outbound
+ // bytes (and connection-teardown requests) to a SessionWriter, which is
+ // implemented by the RPC transport.
+ //
+ // IMPORTANT: Sessions may invoke these methods from worker threads, so
+ // implementations MUST be thread-safe and must marshal any socket operations
+ // onto their I/O thread if required.
+ class SessionWriter
+ {
+ public:
+ virtual ~SessionWriter() = default;
+
+ // `id` throughout is the connection identifier the session was created
+ // with (ccf::tls::ConnID).
+
+ // Queue bytes to be written to the socket associated with `id`. Ownership
+ // is transferred, so that a response which may be arbitrarily large is
+ // moved through to the transport rather than copied again.
+ //
+ // Fire-and-forget: there is currently no backpressure signal.
+ //
+ // FUTURE: to surface genuine TCP-layer backpressure, an implementation
+ // should report when a connection's pending-write queue exceeds a watermark
+ // (tracking per-connection queued bytes) and return a writable/would-block
+ // status here.
+ virtual void write_outbound(int64_t id, std::vector&& data) = 0;
+
+ // Tear down the connection: stop the underlying socket and drop the
+ // session.
+ virtual void close_socket(int64_t id) = 0;
+
+ // Report that `bytes` of previously delivered inbound data have now been
+ // processed. The transport uses this to decide when it may read more: it
+ // stops reading once the node is holding more unprocessed inbound data
+ // than it is willing to, and resumes as sessions catch up. Without it a
+ // client could make the node queue work faster than it retires it, for as
+ // long as it liked.
+ //
+ // A session which does not report is not penalised beyond its own
+ // connection - the transport releases whatever is still outstanding when
+ // the connection closes - so this defaults to a no-op.
+ virtual void inbound_consumed(int64_t /*id*/, size_t /*bytes*/) {}
+ };
}
\ No newline at end of file
diff --git a/include/ccf/node/startup_config.h b/include/ccf/node/startup_config.h
index 3d6cef413798..1dd33d0dfe32 100644
--- a/include/ccf/node/startup_config.h
+++ b/include/ccf/node/startup_config.h
@@ -29,6 +29,12 @@ namespace ccf
ccf::ds::SizeString historical_cache_soft_limit = {"512MB"};
+ // How long an idle RPC (client TLS) connection is kept before it is closed.
+ // std::nullopt disables idle closure (connections are never closed for
+ // being idle).
+ std::optional idle_connection_timeout =
+ ccf::ds::TimeString("60s");
+
ccf::consensus::Configuration consensus = {};
ccf::NodeInfoNetwork network;
diff --git a/include/ccf/research/custom_protocol_subsystem_interface.h b/include/ccf/research/custom_protocol_subsystem_interface.h
index 53092b6340b2..d9f89f069128 100644
--- a/include/ccf/research/custom_protocol_subsystem_interface.h
+++ b/include/ccf/research/custom_protocol_subsystem_interface.h
@@ -16,7 +16,6 @@ namespace ccf
{
namespace tls
{
- class Context;
using ConnID = int64_t;
}
@@ -24,7 +23,7 @@ namespace ccf
{
public:
using CreateSessionFn = std::function(
- ccf::tls::ConnID, const std::unique_ptr&&)>;
+ ccf::tls::ConnID, ccf::SessionWriter&)>;
~CustomProtocolSubsystemInterface() override = default;
@@ -41,7 +40,7 @@ namespace ccf
virtual std::shared_ptr create_session(
const std::string& protocol_name,
ccf::tls::ConnID conn_id,
- const std::unique_ptr&& ctx) = 0;
+ ccf::SessionWriter& writer) = 0;
struct Essentials
{
diff --git a/python/pyproject.toml b/python/pyproject.toml
index e8ee11b20a3d..18462482f034 100644
--- a/python/pyproject.toml
+++ b/python/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "ccf"
-version = "7.0.12"
+version = "7.0.13"
authors = [
{ name="CCF Team", email="CCF-Sec@microsoft.com" },
]
diff --git a/src/clients/rpc_tls_client.h b/src/clients/rpc_tls_client.h
index a4a8251ef945..80037ca0e642 100644
--- a/src/clients/rpc_tls_client.h
+++ b/src/clients/rpc_tls_client.h
@@ -8,6 +8,7 @@
#include "tls_client.h"
#define FMT_HEADER_ONLY
+#include
#include
#include
#include
@@ -90,7 +91,7 @@ namespace client
return call_raw(prep.encoded);
}
- std::optional last_response;
+ std::deque pending_responses;
public:
using TlsClient::TlsClient;
@@ -206,20 +207,20 @@ namespace client
Response read_response()
{
- last_response = std::nullopt;
-
- while (!last_response.has_value())
+ while (pending_responses.empty())
{
const auto next = read_all();
parser.execute(next.data(), next.size());
}
- return std::move(last_response.value());
+ auto response = std::move(pending_responses.front());
+ pending_responses.pop_front();
+ return response;
}
std::optional read_response_non_blocking()
{
- if (bytes_available())
+ if (!pending_responses.empty() || bytes_available())
{
return read_response();
}
@@ -232,8 +233,8 @@ namespace client
ccf::http::HeaderMap&& headers,
std::vector&& body) override
{
- last_response = {
- next_recv_id++, status, std::move(headers), std::move(body)};
+ pending_responses.push_back(
+ {next_recv_id++, status, std::move(headers), std::move(body)});
}
void set_prefix(const std::string& prefix_)
diff --git a/src/clients/tls/README.md b/src/clients/tls/README.md
new file mode 100644
index 000000000000..763759b37a26
--- /dev/null
+++ b/src/clients/tls/README.md
@@ -0,0 +1,14 @@
+# TLS certificate helpers (client tooling only)
+
+These helpers configure an OpenSSL client with a trusted root and an owned
+identity:
+
+- `CA` parses one or more root certificates and populates an `SSL_CTX`'s
+ trusted certificate store.
+- `Cert` holds a client's own certificate and private key, and applies them
+ (and the peer verification mode) to an `SSL_CTX` or `SSL`.
+
+Their only consumer is `TlsClient` in the parent directory, used by the C++
+test and perf clients. **No node code uses them.** The node's inbound TLS is
+handled by `src/host/tls`, which builds its own `SSL_CTX`, and its outbound
+requests go through libcurl.
diff --git a/src/tls/ca.h b/src/clients/tls/ca.h
similarity index 100%
rename from src/tls/ca.h
rename to src/clients/tls/ca.h
diff --git a/src/tls/cert.h b/src/clients/tls/cert.h
similarity index 99%
rename from src/tls/cert.h
rename to src/clients/tls/cert.h
index 5e7776e3e058..c8cd67fe9319 100644
--- a/src/tls/cert.h
+++ b/src/clients/tls/cert.h
@@ -3,9 +3,9 @@
#pragma once
#include "ccf/crypto/openssl/openssl_wrappers.h"
+#include "clients/tls/ca.h"
#include "crypto/openssl/ec_key_pair.h"
#include "ds/internal_logger.h"
-#include "tls/ca.h"
#include
#include
diff --git a/src/clients/tls/test/main.cpp b/src/clients/tls/test/main.cpp
new file mode 100644
index 000000000000..b71c1db7363b
--- /dev/null
+++ b/src/clients/tls/test/main.cpp
@@ -0,0 +1,129 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the Apache 2.0 License.
+
+// Unit tests for the certificate helpers used by the C++ test clients, which
+// configure an OpenSSL context and connection with a trusted root and an owned
+// identity. The TLS transport itself is tested in
+// src/host/test/openssl_server_test.cpp.
+
+#include "ccf/crypto/ec_key_pair.h"
+#include "ccf/crypto/verifier.h"
+#include "ccf/ds/x509_time_fmt.h"
+#include "clients/tls/ca.h"
+#include "clients/tls/cert.h"
+#include "crypto/certs.h"
+
+#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
+#include
+#include
+#include
+#include
+#include
+
+namespace
+{
+ constexpr size_t certificate_validity_period_days = 365;
+
+ std::string valid_from_yesterday()
+ {
+ using namespace std::literals;
+ return ccf::ds::to_x509_time_string(std::chrono::system_clock::now() - 24h);
+ }
+
+ ccf::crypto::Pem generate_self_signed_cert(
+ const ccf::crypto::ECKeyPairPtr& kp, const std::string& name)
+ {
+ return ccf::crypto::create_self_signed_cert(
+ kp, name, {}, valid_from_yesterday(), certificate_validity_period_days);
+ }
+
+ struct NetworkCA
+ {
+ ccf::crypto::ECKeyPairPtr kp;
+ ccf::crypto::Pem cert;
+ };
+
+ /// Get self-signed CA certificate.
+ NetworkCA get_ca()
+ {
+ auto kp = ccf::crypto::make_ec_key_pair();
+ return {kp, generate_self_signed_cert(kp, "CN=issuer")};
+ }
+
+ /// Creates a ::tls::Cert endorsed by the given CA.
+ std::unique_ptr<::tls::Cert> get_dummy_cert(
+ NetworkCA& net_ca, const std::string& name, bool auth_required = true)
+ {
+ auto ca = std::make_unique<::tls::CA>(net_ca.cert.str());
+
+ // Create a signing request and sign with the CA
+ auto kp = ccf::crypto::make_ec_key_pair();
+ auto crt = ccf::crypto::create_endorsed_cert(
+ kp,
+ "CN=" + name,
+ {},
+ valid_from_yesterday(),
+ certificate_validity_period_days,
+ net_ca.kp->private_key_pem(),
+ net_ca.cert);
+
+ // Verify node certificate with the CA's certificate
+ auto v = ccf::crypto::make_verifier(crt);
+ REQUIRE(v->verify_certificate({&net_ca.cert}));
+
+ return std::make_unique<::tls::Cert>(
+ std::move(ca), crt, kp->private_key_pem(), std::nullopt, auth_required);
+ }
+}
+
+TEST_CASE("CA configures trusted certificate store")
+{
+ auto ca = get_ca();
+ ::tls::CA trusted_ca(ca.cert.str(), true);
+ ccf::crypto::OpenSSL::Unique_SSL_CTX ctx(TLS_method());
+
+ trusted_ca.configure_trusted_cert_store(ctx);
+
+ auto* store = SSL_CTX_get_cert_store(ctx);
+ REQUIRE(store != nullptr);
+ auto* params = X509_STORE_get0_param(store);
+ REQUIRE(params != nullptr);
+ REQUIRE(
+ (X509_VERIFY_PARAM_get_flags(params) & X509_V_FLAG_PARTIAL_CHAIN) != 0);
+}
+
+TEST_CASE("Cert configures TLS verification and own certificate")
+{
+ auto ca = get_ca();
+
+ SUBCASE("auth_required requires a peer certificate")
+ {
+ auto cert = get_dummy_cert(ca, "server");
+ ccf::crypto::OpenSSL::Unique_SSL_CTX ctx(TLS_method());
+
+ cert->configure_context(ctx);
+ ccf::crypto::OpenSSL::Unique_SSL ssl(ctx);
+ cert->configure_connection(ssl);
+
+ constexpr auto expected_verify_mode =
+ SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
+ REQUIRE(SSL_CTX_get_verify_mode(ctx) == expected_verify_mode);
+ REQUIRE(SSL_get_verify_mode(ssl) == expected_verify_mode);
+ REQUIRE(SSL_CTX_get0_certificate(ctx) != nullptr);
+ REQUIRE(SSL_get_certificate(ssl) != nullptr);
+ }
+
+ SUBCASE("without auth_required a peer certificate is requested, not required")
+ {
+ auto cert = get_dummy_cert(ca, "server", false);
+ ccf::crypto::OpenSSL::Unique_SSL_CTX ctx(TLS_method());
+
+ cert->configure_context(ctx);
+ ccf::crypto::OpenSSL::Unique_SSL ssl(ctx);
+ cert->configure_connection(ssl);
+
+ // The connection inherits the context's verification mode
+ REQUIRE((SSL_get_verify_mode(ssl) & SSL_VERIFY_PEER) != 0);
+ REQUIRE((SSL_get_verify_mode(ssl) & SSL_VERIFY_FAIL_IF_NO_PEER_CERT) == 0);
+ }
+}
diff --git a/src/clients/tls_client.h b/src/clients/tls_client.h
index bdeee21ca057..2b574bee4fae 100644
--- a/src/clients/tls_client.h
+++ b/src/clients/tls_client.h
@@ -3,9 +3,9 @@
#pragma once
#include "ccf/crypto/openssl/openssl_wrappers.h"
+#include "clients/tls/ca.h"
+#include "clients/tls/cert.h"
#include "ds/internal_logger.h"
-#include "tls/ca.h"
-#include "tls/cert.h"
#include
#include
diff --git a/src/common/configuration.h b/src/common/configuration.h
index d11b775b26e6..05d47975f0ae 100644
--- a/src/common/configuration.h
+++ b/src/common/configuration.h
@@ -143,7 +143,8 @@ namespace ccf
files_cleanup,
node_to_node_message_limit,
historical_cache_soft_limit,
- identity_history_fetch);
+ identity_history_fetch,
+ idle_connection_timeout);
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(RecoveryDecisionProtocolConfig);
DECLARE_JSON_REQUIRED_FIELDS(
diff --git a/src/ds/work_beacon.h b/src/ds/work_beacon.h
index 277f505f19d7..c603d1bf2d7a 100644
--- a/src/ds/work_beacon.h
+++ b/src/ds/work_beacon.h
@@ -55,6 +55,24 @@ namespace ccf::ds
condition_variable.notify_all();
}
+
+ void notify_work_available_coalesced()
+ {
+ bool notify = false;
+ {
+ std::lock_guard lock(mutex);
+ if (work_available == 0)
+ {
+ work_available = 1;
+ notify = true;
+ }
+ }
+
+ if (notify)
+ {
+ condition_variable.notify_all();
+ }
+ }
};
using WorkBeaconPtr = std::shared_ptr;
diff --git a/src/enclave/abstract_rpc_sessions.h b/src/enclave/abstract_rpc_sessions.h
new file mode 100644
index 000000000000..f1cc85c7c062
--- /dev/null
+++ b/src/enclave/abstract_rpc_sessions.h
@@ -0,0 +1,42 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the Apache 2.0 License.
+#pragma once
+
+#include "ccf/crypto/pem.h"
+#include "ccf/service/node_info_network.h"
+#include "forwarder_types.h"
+#include "node/session_metrics.h"
+
+#include
+#include
+
+namespace ccf
+{
+ class CustomProtocolSubsystem;
+ class CommitCallbackSubsystem;
+
+ // The slice of RPC session management that the node (NodeState, frontends,
+ // Enclave, jwt refresh) depends on, independent of how connections are
+ // actually serviced. RPCConnectionManager implements this, so node-side code
+ // can hold a reference without depending on the concrete networking backend.
+ class AbstractRPCSessions : public AbstractRPCResponder
+ {
+ public:
+ ~AbstractRPCSessions() override = default;
+
+ virtual ccf::SessionMetrics get_session_metrics() = 0;
+
+ virtual void set_node_cert(
+ const ccf::crypto::Pem& cert, const ccf::crypto::Pem& pk) = 0;
+ virtual void set_network_cert(
+ const ccf::crypto::Pem& cert, const ccf::crypto::Pem& pk) = 0;
+
+ virtual void update_listening_interface_options(
+ const ccf::NodeInfoNetwork& node_info) = 0;
+
+ virtual void set_custom_protocol_subsystem(
+ std::shared_ptr cpss) = 0;
+ virtual void set_commit_callbacks_subsystem(
+ std::shared_ptr ccss) = 0;
+ };
+}
diff --git a/src/enclave/enclave.h b/src/enclave/enclave.h
index 5e864b7f9e0e..9485b491a50b 100644
--- a/src/enclave/enclave.h
+++ b/src/enclave/enclave.h
@@ -11,6 +11,7 @@
#include "ds/oversized.h"
#include "ds/work_beacon.h"
#include "host/ledger.h"
+#include "host/rpc_connection_manager.h"
#include "indexing/enclave_lfs_access.h"
#include "indexing/historical_transaction_fetcher.h"
#include "interface.h"
@@ -34,7 +35,6 @@
#include "node/rpc/user_frontend.h"
#include "node/signature_cache_subsystem.h"
#include "rpc_map.h"
-#include "rpc_sessions.h"
#include "tasks/worker.h"
namespace ccf
@@ -48,7 +48,7 @@ namespace ccf
ccf::ds::WorkBeaconPtr work_beacon;
ccf::NetworkState network;
std::shared_ptr rpc_map;
- std::shared_ptr rpcsessions;
+ std::shared_ptr rpcsessions;
std::unique_ptr node;
ringbuffer::WriterPtr to_host = nullptr;
std::chrono::high_resolution_clock::time_point last_tick_time;
@@ -92,7 +92,7 @@ namespace ccf
writer_factory(std::move(writer_factory_)),
work_beacon(std::move(work_beacon_)),
rpc_map(std::make_shared()),
- rpcsessions(std::make_shared(*writer_factory, rpc_map))
+ rpcsessions(std::make_shared(rpc_map))
{
to_host = writer_factory->create_writer_to_outside();
@@ -184,23 +184,110 @@ namespace ccf
signature_cache,
sig_tx_interval,
sig_ms_interval);
+
+ ccf::tasks::get_main_job_board().set_work_beacon(work_beacon);
}
~Enclave()
{
+ ccf::tasks::get_main_job_board().set_work_beacon(nullptr);
LOG_TRACE_FMT("Shutting down enclave");
}
CreateNodeStatus create_new_node(
StartType start_type_,
- const ccf::StartupConfig& ccf_config_,
+ ccf::StartupConfig ccf_config_,
std::vector& node_cert,
- std::vector& service_cert)
+ std::vector& service_cert,
+ std::vector& rpc_addresses)
{
start_type = start_type_;
rpcsessions->update_listening_interface_options(ccf_config_.network);
+ // Idle RPC connections are closed after this period (nullopt = never).
+ std::optional rpc_idle_timeout;
+ if (ccf_config_.idle_connection_timeout.has_value())
+ {
+ rpc_idle_timeout = std::chrono::milliseconds(
+ ccf_config_.idle_connection_timeout->count_ms());
+ }
+ rpcsessions->set_idle_connection_timeout(rpc_idle_timeout);
+
+ // Bind and start listening on each configured RPC interface. TLS is now
+ // terminated in the connection: an interface whose certificate is not yet
+ // available refuses connections until set_*_cert provides one (a joining
+ // node receives the service cert later). Ephemeral ports (bind ":0") are
+ // assigned here, so the resolved addresses are reported back to the host
+ // (which writes the rpc addresses file).
+ {
+ // An object rather than a default-constructed (null) json, so that a
+ // node with no RPC interfaces still writes a well-formed, empty map.
+ nlohmann::json resolved_rpc_addresses = nlohmann::json::object();
+
+ // Bind interfaces with an explicit (non-zero) port before those
+ // requesting an ephemeral port (port 0 or unspecified). Multiple
+ // interfaces on a node can share a single host address (for example
+ // the sole ::1 IPv6 loopback), and if an ephemeral interface is bound
+ // first the OS may assign it the exact port that another interface is
+ // configured to bind, making that later bind fail with "address
+ // already in use".
+ const auto is_ephemeral = [](const auto& interface) {
+ const auto port =
+ ccf::split_net_address(interface.bind_address).second;
+ return port.empty() || port == "0";
+ };
+ std::vector ordered_interface_names;
+ ordered_interface_names.reserve(
+ ccf_config_.network.rpc_interfaces.size());
+ for (const auto& [name, interface] : ccf_config_.network.rpc_interfaces)
+ {
+ if (!is_ephemeral(interface))
+ {
+ ordered_interface_names.push_back(name);
+ }
+ }
+ for (const auto& [name, interface] : ccf_config_.network.rpc_interfaces)
+ {
+ if (is_ephemeral(interface))
+ {
+ ordered_interface_names.push_back(name);
+ }
+ }
+
+ for (const auto& name : ordered_interface_names)
+ {
+ auto& interface = ccf_config_.network.rpc_interfaces.at(name);
+ const auto [host, port] =
+ ccf::split_net_address(interface.bind_address);
+ // UDP interfaces use the datagram (echo) path; TCP interfaces the
+ // OpenSSL stream path.
+ const uint16_t bound = (interface.protocol == "udp") ?
+ rpcsessions->listen_udp(name, host, port) :
+ rpcsessions->listen(name, host, port);
+ interface.bind_address =
+ ccf::make_net_address(host, std::to_string(bound));
+
+ if (interface.published_address.empty())
+ {
+ interface.published_address = interface.bind_address;
+ }
+ else
+ {
+ const auto [phost, pport] =
+ ccf::split_net_address(interface.published_address);
+ if (pport == "0")
+ {
+ interface.published_address =
+ ccf::make_net_address(phost, std::to_string(bound));
+ }
+ }
+ resolved_rpc_addresses[name] = interface.bind_address;
+ }
+ const auto dumped = resolved_rpc_addresses.dump();
+ rpc_addresses.assign(dumped.begin(), dumped.end());
+ }
+
node->set_n2n_message_limit(ccf_config_.node_to_node_message_limit);
historical_state_cache->set_soft_cache_limit(
@@ -392,18 +479,20 @@ namespace ccf
}
});
- rpcsessions->register_message_handlers(bp.get_dispatcher());
-
// Maximum number of inbound ringbuffer messages which will be
// processed in a single iteration
static constexpr size_t max_messages = 256;
+ bool should_wait_for_work = true;
while (!bp.get_finished())
{
- // Wait until the host indicates that some ringbuffer messages are
- // available, but wake at least every 100ms to check thread messages
- work_beacon->wait_for_work_with_timeout(
- std::chrono::milliseconds(100));
+ if (should_wait_for_work)
+ {
+ // Wait until the host indicates that some ringbuffer messages or
+ // tasks are available, but wake at least every 100ms.
+ work_beacon->wait_for_work_with_timeout(
+ std::chrono::milliseconds(100));
+ }
// First, read some messages from the ringbuffer
auto read = bp.read_n(max_messages, circuit->read_from_outside());
@@ -422,6 +511,10 @@ namespace ccf
}
task = job_board.get_task();
}
+ // Hitting the task budget may leave queued work behind. Continue
+ // immediately rather than consuming the only coalesced wake and then
+ // sleeping with a non-empty JobBoard.
+ should_wait_for_work = tasks_done < max_messages;
// If no messages were read from the ringbuffer and tasks were
// executed, idle
@@ -431,6 +524,11 @@ namespace ccf
}
}
+ LOG_INFO_FMT("Stopping RPC transports");
+ // The host is still running the libuv loop at this point - it only
+ // exits once we send AdminMessage::stopped below.
+ rpcsessions->stop(asynchost::OpenSSLServer::LoopState::Running);
+
LOG_INFO_FMT("Enclave stopped successfully. Stopping host...");
RINGBUFFER_WRITE_MESSAGE(AdminMessage::stopped, to_host);
diff --git a/src/enclave/entry_points.h b/src/enclave/entry_points.h
index 8dc35be490d7..9fcca727ab02 100644
--- a/src/enclave/entry_points.h
+++ b/src/enclave/entry_points.h
@@ -15,6 +15,7 @@ namespace ccf
const ccf::StartupConfig& ccf_config,
std::vector& node_cert,
std::vector& service_cert,
+ std::vector& rpc_addresses,
StartType start_type,
ccf::LoggerLevel log_level,
size_t num_worker_thread,
diff --git a/src/enclave/main.cpp b/src/enclave/main.cpp
index fe1b11530989..cc556daff90e 100644
--- a/src/enclave/main.cpp
+++ b/src/enclave/main.cpp
@@ -30,6 +30,7 @@ namespace ccf
const ccf::StartupConfig& ccf_config,
std::vector& node_cert,
std::vector& service_cert,
+ std::vector& rpc_addresses,
StartType start_type,
ccf::LoggerLevel log_level,
size_t num_worker_threads,
@@ -132,7 +133,7 @@ namespace ccf
try
{
status = enclave->create_new_node(
- start_type, ccf_config, node_cert, service_cert);
+ start_type, ccf_config, node_cert, service_cert, rpc_addresses);
}
catch (...)
{
diff --git a/src/enclave/no_more_sessions.h b/src/enclave/no_more_sessions.h
new file mode 100644
index 000000000000..a37a431b428a
--- /dev/null
+++ b/src/enclave/no_more_sessions.h
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the Apache 2.0 License.
+#pragma once
+
+#include "ccf/odata_error.h"
+
+namespace ccf
+{
+ // Session wrapper used when an interface is over its soft session limit.
+ //
+ // It sends a single 503 response explaining that the service is busy, then
+ // closes the connection. It is templated on the concrete server session type
+ // (HTTPServerSession / HTTP2ServerSession) so it reuses that session's
+ // response machinery.
+ template
+ class NoMoreSessionsImpl : public Base
+ {
+ public:
+ template
+ NoMoreSessionsImpl(Ts&&... ts) : Base(std::forward(ts)...)
+ {}
+
+ void handle_incoming_data_thread(std::vector&& /*data*/) override
+ {
+ // The transport already terminated TLS, so we can respond immediately.
+ Base::send_odata_error_response(ccf::ErrorDetails{
+ HTTP_STATUS_SERVICE_UNAVAILABLE,
+ ccf::errors::SessionCapExhausted,
+ "Service is currently busy and unable to serve new connections"});
+
+ Base::close_session();
+ }
+ };
+}
diff --git a/src/enclave/rpc_sessions.h b/src/enclave/rpc_sessions.h
deleted file mode 100644
index ec919cda8041..000000000000
--- a/src/enclave/rpc_sessions.h
+++ /dev/null
@@ -1,632 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the Apache 2.0 License.
-#pragma once
-
-#include "ccf/pal/locking.h"
-#include "ccf/service/node_info_network.h"
-#include "ds/internal_logger.h"
-#include "ds/serialized.h"
-#include "enclave/session.h"
-#include "forwarder_types.h"
-#include "http/http2_session.h"
-#include "http/http_responder.h"
-#include "http/http_session.h"
-#include "node/rpc/custom_protocol_subsystem.h"
-#include "node/session_metrics.h"
-#include "rpc_handler.h"
-#include "tls/cert.h"
-#include "tls/context.h"
-#include "tls/plaintext_server.h"
-#include "tls/server.h"
-#include "udp/msg_types.h"
-
-// NB: This should be HTTP3 including QUIC, but this is
-// ok for now, as we only have an echo service for now
-#include "quic/quic_session.h"
-
-#include
-#include