Skip to content

New RPCConnectionManager: Single unified manager, no ringbuffer, OpenSSL-owned sockets - #8117

Open
Eddy Ashton (eddyashton) wants to merge 75 commits into
mainfrom
rpc_connection_manager
Open

New RPCConnectionManager: Single unified manager, no ringbuffer, OpenSSL-owned sockets#8117
Eddy Ashton (eddyashton) wants to merge 75 commits into
mainfrom
rpc_connection_manager

Conversation

@eddyashton

@eddyashton Eddy Ashton (eddyashton) commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

This PR replaces the old split RPC networking path with an OpenSSL-native connection layer. Previously, socket handling lived in host-side libuv code while TLS and protocol sessions were driven through enclave-side ringbuffer messages and memory BIOs. With CCF now running as a single process, that split is no longer useful, so RPC sockets, TLS, protocol session creation, and per-interface policy now live behind a single RPC connection manager.

TLS now terminates at the connection layer. Protocol sessions receive plaintext and write responses through a SessionWriter, so HTTP, HTTP/2, and custom protocols no longer own TLS state directly. This removes the RPC ringbuffer message path, the memory-BIO TLS session layer, and the old libuv RPC connection containers.

The new transport uses non-blocking sockets bound directly to OpenSSL, and splits the work in two. The existing host libuv loop owns the server's own state - accepting connections, uv_poll_t registration, the SSL_CTX, idle connection cleanup via uv_timer_t, and closing file descriptors - and performs no SSL operations itself. Every SSL operation for a connection instead runs on that connection's own OrderedTasks queue, keeping handshakes and bulk encryption off the loop thread. The loop only schedules a pass over a connection, driven either by file descriptor readiness or by a cross-thread request (a queued write, a close, or a certificate update) marshalled through uv_async_t. A connection is serviced by at most one pass at a time. Per-interface behavior such as certificates, session caps, metrics, HTTP parser settings, and custom protocol dispatch is centralized in RPCConnectionManager.

Session caps are applied when a connection is accepted, before any TLS state exists, rather than when its first request arrives. max_open_sessions_hard is documented as a bound on connections, and counting at first-request time missed any client that completed the TCP and TLS handshakes and then went silent while still holding a file descriptor and TLS state.

Node outbound requests are outside this transport and use libcurl.

UDP remains as a small datagram transport driven by uv_poll_t. The temporary QUIC/UDP echo behavior is stateless, so it consumes no session and no interface capacity, while custom UDP protocols are routed to per-peer sessions. Native QUIC is still future work and requires OpenSSL 3.5 or later.

Structural Breakdown

OpenSSLServer is the low-level inbound connection transport. It owns the listening and accepted socket file descriptors, SSL objects, uv_poll_t handles, read/write buffers, handshake state, graceful-close state, certificate reload requests, and idle-timeout sweeping.

OpenSSLSessionManager bridges transport connections to ccf::Session. It lazily creates sessions for inbound connections, forwards plaintext bytes into sessions, implements SessionWriter, and reports connection closure back to the owner.

RPCConnectionManager is the higher-level RPC owner. It replaces the old RPC session container and owns one transport bridge per TCP interface, plus UDP interface state. It applies per-interface admission and caps, certificates, parser settings, application protocol selection, session metrics, custom protocol routing, and UDP peer demultiplexing.

SessionWriter, Session, and PlaintextSession form the new session boundary. Sessions no longer encrypt or decrypt; they parse plaintext and emit plaintext responses to their writer. HTTP/1 and HTTP/2 sessions now use this boundary.

CustomProtocolSubsystemInterface now creates sessions from (ConnID, SessionWriter&) rather than a TLS context. This matches the new layering: custom protocols see plaintext and write through the transport-neutral writer.

DatagramServer is the UDP socket transport. It uses a uv_poll_t handle on the existing libuv loop. RPCConnectionManager echoes datagrams directly for the temporary QUIC behavior, and maps UDP peers to sessions for custom datagram protocols.

Startup wiring moved accordingly. The enclave creates and owns the RPC manager, binds RPC interfaces, resolves actual bound addresses including ephemeral ports, and reports those addresses back through the enclave entry point so the host can write the RPC addresses file.

The removed files are the old RPC transport stack: RPCSessions, TLSSession, host RPC connections, legacy UDP plumbing, the old QUIC session, and the TCP/UDP ringbuffer message types that were specific to the split RPC path. Ledger, consensus, and node-to-node uses of ringbuffer and libuv are not part of this change.

…ransport cert-deferred listening + ALPN + outbound client, RPCConnectionManager (AbstractRPCSessions). Not yet wired into enclave/run.cpp.
…wire RPCConnectionManager into enclave.h/run.cpp, delete RPCSessions/rpc_connections/tls_session. Full build green, 53/53 unit tests pass.
…:Cert::use) and request client cert on inbound for caller auth; add peer-cert capture test. Full build green, unit tests pass.
…xing localhost/[::1] interfaces (cpp, cpp_cose_only, common_ipv6 e2e). Add localhost/IPv6 binding tests.
… large response queued just before close_socket() is not truncated (fixes cpp/cpp_cose_only receipt 'server disconnected'). Add truncation test.
…e (ERR_clear_error) before each SSL op so a stale error from one connection cannot poison SSL_get_error for another (root cause of cpp/cpp_cose_only 'server disconnected'). Add persistent-connection + peer-cert tests.
… handler; branch udp interfaces to listen_udp. Clearly marked QUIC extension points (substrate for OpenSSL >=3.5 native QUIC). e2e_logging udp echo passes; full suite green.
…ned quic_session.h/src/quic, udp.h + udp/msg_types.h + UDPImpl vestiges in run.cpp, dead RPC ringbuffer message enums (keep tcp::ConnID). Drop old-implementation comments. Fix build after cert.h use->configure_ssl rename + commit-callback include.
…Config (so the enclave-side RPC manager receives it); track per-connection last_active in OpenSSLServer and sweep idle connections off the epoll timeout. Plumbed manager->bridge->server. idletimeout e2e passes.
Eddy Ashton (eddyashton) and others added 18 commits August 7, 2026 10:28
Notify the enclave work beacon when transport tasks enter the JobBoard, while preserving direct worker handoff and coalescing redundant wakeups. Keep bounded task drains moving immediately when a backlog remains.\n\nRefs #8117\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…code

- Modify CMakeLists.txt for better configuration.
- Refactor rpc_tls_client.h for improved clarity and functionality.
- Enhance openssl_server_test.cpp with additional test cases.
RPC task workers can query consensus state concurrently with Raft message processing. Publish a coherent query snapshot without taking the Raft lock from KV-backed endpoints, avoiding both data races and KV/Raft lock inversion.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

# Conflicts:
#	CMakeLists.txt
#	src/tls/test/main.cpp
@achamayou
Amaury Chamayou (achamayou) marked this pull request as ready for review August 19, 2026 10:12
Comment thread src/clients/tls/test/main.cpp
Comment thread src/consensus/aft/raft.h Outdated
// Public consensus queries may run on task workers while Raft messages are
// processed on the enclave main thread. Keep these small query fields
// independently synchronized so endpoint transactions do not need to take
// state->lock and invert the KV/Raft lock order.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels risky on its own, enough not to bundle it with this change I would say, can we keep it as a follow-up performance improvement PR?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is a legitimate risk, and somehow causing consistent TSAN failures on this PR that main escapes. Tackling this separately in #8181.

Comment thread src/enclave/abstract_rpc_sessions.h Outdated
Comment thread src/enclave/enclave.h
These changes were added while chasing TSAN failures exposed during the RPC connection manager work, but they are broader Raft synchronization changes and should be handled separately rather than bundled into this PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment on lines +492 to +497
// Disable renegotiation to avoid DoS
SSL_CTX_set_options(
c,
SSL_OP_CIPHER_SERVER_PREFERENCE |
SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION |
SSL_OP_NO_RENEGOTIATION);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sharing this SSL_CTX across connections makes OpenSSL's resumption defaults effective for the first time. CCF should explicitly require a fresh authenticated key exchange for every connection:

  • TLS 1.2 resumption reuses the prior master secret without a fresh ECDHE exchange, so it does not provide fresh forward secrecy. Disable both stateful session IDs and stateless RFC 5077 tickets.
  • TLS 1.3 defaults to issuing two NewSessionTicket messages, enabling PSK resumption. Set the count to zero so every TLS 1.3 connection also performs a full handshake.
  • Keep 0-RTT explicitly disabled. Early data is replayable, lacks fresh forward secrecy, and can arrive before current client authentication completes, which is unsafe for state-changing RPC endpoints.

Let's encode this as part of the inbound TLS policy rather than relying on OpenSSL defaults.

Suggested change
// Disable renegotiation to avoid DoS
SSL_CTX_set_options(
c,
SSL_OP_CIPHER_SERVER_PREFERENCE |
SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION |
SSL_OP_NO_RENEGOTIATION);
// CCF requires a fresh authenticated key exchange for every connection.
// Disable stateful session caching, stateless tickets, TLS 1.3 tickets,
// and early data so resumption cannot weaken forward secrecy or enable
// replayable pre-authentication requests.
SSL_CTX_set_session_cache_mode(c, SSL_SESS_CACHE_OFF);
if (SSL_CTX_set_num_tickets(c, 0) != 1)
{
return fail("SSL_CTX_set_num_tickets");
}
if (SSL_CTX_set_max_early_data(c, 0) != 1)
{
return fail("SSL_CTX_set_max_early_data");
}
// Disable renegotiation and stateless session tickets.
SSL_CTX_set_options(
c,
SSL_OP_CIPHER_SERVER_PREFERENCE |
SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION |
SSL_OP_NO_RENEGOTIATION | SSL_OP_NO_TICKET);

::tls::CA ca(pem.value());
ccf::crypto::OpenSSL::Unique_BIO bio(
pem.value().data(), pem.value().size());
ccf::crypto::OpenSSL::Unique_X509 cert(bio, true);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This stricter behavior is reasonable, but it is a user-facing compatibility change. The removed tls::CA path treated an empty string as a no-op, so ccf.crypto.isValidX509CertBundle("") returned true; this replacement returns false, which can cause governance proposals that previously passed validation to be rejected. Let's add a CHANGELOG entry describing the intentional behavior change and cover the empty-input case explicitly in the tests.

Co-authored-by: Amaury Chamayou <amchamay@microsoft.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bench-ab run-long-test Run Long Test job

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants