This is an automated email from the ASF dual-hosted git repository.

hubcio pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git


The following commit(s) were added to refs/heads/master by this push:
     new f77d64b2f fix(server): distribute TCP-TLS and WSS connections across 
shards (#4193)
f77d64b2f is described below

commit f77d64b2faf01c11e77242c1a073b6b9b200d7aa
Author: Hubert Gruszecki <[email protected]>
AuthorDate: Tue Sep 15 16:07:08 2026 +0200

    fix(server): distribute TCP-TLS and WSS connections across shards (#4193)
---
 core/integration/tests/server/specific.rs          | 187 ++++++++++++-
 core/message_bus/src/client_listener/mod.rs        |  31 ++-
 core/message_bus/src/client_listener/tcp_tls.rs    |  27 +-
 core/message_bus/src/client_listener/wss.rs        |  16 +-
 core/message_bus/src/fd_transfer.rs                |  26 +-
 core/message_bus/src/installer/common.rs           |   7 +-
 core/message_bus/src/installer/mod.rs              |  56 +++-
 core/message_bus/src/installer/replica.rs          |  14 +-
 core/message_bus/src/installer/tcp.rs              |   5 +
 core/message_bus/src/installer/tcp_tls.rs          |   9 +-
 core/message_bus/src/installer/wss.rs              |   5 +-
 core/message_bus/src/lib.rs                        |  52 ++--
 .../src/lifecycle/connection_registry.rs           |  11 +-
 core/message_bus/src/replica/io.rs                 |  45 ++-
 core/message_bus/src/transports/tcp_tls.rs         |   6 +-
 core/message_bus/src/transports/wss.rs             |  28 +-
 core/message_bus/tests/common/mod.rs               | 111 +++++++-
 core/message_bus/tests/tcp_tls_client_listener.rs  |   5 +
 core/message_bus/tests/wss_client_listener.rs      | 108 ++++++++
 core/server/src/boot/listeners.rs                  |  64 ++---
 core/server/src/boot/mod.rs                        |   7 +-
 core/server/src/dispatch/mod.rs                    |  11 +-
 core/server/src/dispatch/test_support.rs           |  18 +-
 core/shard/src/coordinator.rs                      | 308 +++++++++++++++++++--
 core/shard/src/lib.rs                              |  23 +-
 core/shard/src/router.rs                           |  24 ++
 core/simulator/src/bus.rs                          |  22 +-
 27 files changed, 956 insertions(+), 270 deletions(-)

diff --git a/core/integration/tests/server/specific.rs 
b/core/integration/tests/server/specific.rs
index 5bf367083..3e3281d72 100644
--- a/core/integration/tests/server/specific.rs
+++ b/core/integration/tests/server/specific.rs
@@ -20,33 +20,70 @@ use 
crate::server::scenarios::{reconnect_after_restart_scenario, restart_offset_
 use crate::server::scenarios::{
     segment_rotation_race_scenario, tcp_tls_scenario, websocket_tls_scenario,
 };
+use iggy::prelude::*;
+use integration::harness::TestHarness;
 use integration::iggy_harness;
+use std::collections::HashSet;
+use std::time::Duration;
+use tokio::io::AsyncWriteExt;
+use tokio::net::TcpStream;
+
+const TLS_CLIENTS_PER_SHARD: usize = 2;
+const TLS_TEST_TIMEOUT: Duration = Duration::from_secs(10);
+const TLS_POLL_INTERVAL: Duration = Duration::from_millis(20);
 
 #[iggy_harness(
     test_client_transport = TcpTlsGenerated,
-    server(tls = generated)
+    server(tls = generated, logging.level = "info")
 )]
 async fn tcp_tls_scenario_should_be_valid(harness: &TestHarness) {
-    let client = harness.root_client().await.unwrap();
-    tcp_tls_scenario::run(&client).await;
+    let shard_count = tls_test_shard_count(harness).await;
+    let slow_clients = stalled_tls_clients(harness, shard_count).await;
+    let clients = harness
+        .root_clients(shard_count * TLS_CLIENTS_PER_SHARD)
+        .await
+        .unwrap();
+    for client in &clients {
+        tcp_tls_scenario::run(client).await;
+    }
+    drop(slow_clients);
+    assert_tls_client_sharding_and_cleanup(harness, &clients, "TCP-TLS", 
shard_count).await;
 }
 
 #[iggy_harness(
     test_client_transport = TcpTlsSelfSigned,
-    server(tls = self_signed)
+    server(tls = self_signed, logging.level = "info")
 )]
 async fn tcp_tls_self_signed_scenario_should_be_valid(harness: &TestHarness) {
-    let client = harness.root_client().await.unwrap();
-    tcp_tls_scenario::run(&client).await;
+    let shard_count = tls_test_shard_count(harness).await;
+    let slow_clients = stalled_tls_clients(harness, shard_count).await;
+    let clients = harness
+        .root_clients(shard_count * TLS_CLIENTS_PER_SHARD)
+        .await
+        .unwrap();
+    for client in &clients {
+        tcp_tls_scenario::run(client).await;
+    }
+    drop(slow_clients);
+    assert_tls_client_sharding_and_cleanup(harness, &clients, "TCP-TLS", 
shard_count).await;
 }
 
 #[iggy_harness(
     test_client_transport = WebSocketTlsGenerated,
-    server(websocket_tls = generated)
+    server(websocket_tls = generated, logging.level = "info")
 )]
 async fn websocket_tls_scenario_should_be_valid(harness: &TestHarness) {
-    let client = harness.root_client().await.unwrap();
-    websocket_tls_scenario::run(&client).await;
+    let shard_count = tls_test_shard_count(harness).await;
+    let slow_clients = stalled_tls_clients(harness, shard_count).await;
+    let clients = harness
+        .root_clients(shard_count * TLS_CLIENTS_PER_SHARD)
+        .await
+        .unwrap();
+    for client in &clients {
+        websocket_tls_scenario::run(client).await;
+    }
+    drop(slow_clients);
+    assert_tls_client_sharding_and_cleanup(harness, &clients, "WSS", 
shard_count).await;
 }
 
 #[iggy_harness]
@@ -141,3 +178,135 @@ async fn restart_offset_skip(harness: &mut TestHarness) {
 async fn segment_rotation_scenario(harness: &TestHarness) {
     segment_rotation_race_scenario::run(harness).await;
 }
+
+async fn tls_test_shard_count(harness: &TestHarness) -> usize {
+    tokio::time::timeout(TLS_TEST_TIMEOUT, async {
+        loop {
+            let logs = harness.server().stdout_plain();
+            if let Some(line) = logs
+                .lines()
+                .find(|line| line.contains("server bootstrap dispatched; 
awaiting shard runtimes"))
+            {
+                let shard_count = line
+                    .split_whitespace()
+                    .find_map(|field| field.strip_prefix("shards_count="))
+                    .expect("bootstrap log must carry shards_count")
+                    .parse::<usize>()
+                    .expect("bootstrap shards_count must be numeric");
+                assert!(shard_count > 0, "server must start at least one 
shard");
+                return shard_count;
+            }
+            tokio::time::sleep(TLS_POLL_INTERVAL).await;
+        }
+    })
+    .await
+    .expect("server must report its resolved shard count")
+}
+
+async fn assert_tls_client_sharding_and_cleanup(
+    harness: &TestHarness,
+    clients: &[IggyClient],
+    transport: &str,
+    shard_count: usize,
+) {
+    assert_tls_client_sharding(harness, clients, transport, shard_count).await;
+    for client in clients {
+        client.disconnect().await.unwrap();
+    }
+
+    // Each fresh observer has no partition connection and lives on a different
+    // shard. Seeing every observer proves that the gather includes every 
shard.
+    let observers = harness.root_clients(shard_count).await.unwrap();
+    let observer_ids =
+        assert_tls_client_sharding(harness, &observers, transport, 
shard_count).await;
+    tokio::time::timeout(TLS_TEST_TIMEOUT, async {
+        loop {
+            let connected = observers[0].get_clients().await.unwrap();
+            let connected_ids: HashSet<_> =
+                connected.iter().map(|client| client.client_id).collect();
+            if connected.len() == observers.len() && connected_ids == 
observer_ids {
+                break;
+            }
+            tokio::time::sleep(TLS_POLL_INTERVAL).await;
+        }
+    })
+    .await
+    .expect("every shard must report only its observer after client 
disconnect");
+    for observer in observers {
+        observer.disconnect().await.unwrap();
+    }
+}
+
+async fn assert_tls_client_sharding(
+    harness: &TestHarness,
+    clients: &[IggyClient],
+    transport: &str,
+    shard_count: usize,
+) -> HashSet<u32> {
+    let mut client_ids = HashSet::with_capacity(clients.len());
+    for client in clients {
+        client_ids.insert(client.get_me().await.unwrap().client_id);
+    }
+    assert_eq!(client_ids.len(), clients.len(), "client IDs must be unique");
+
+    // The wire exposes only the sequence tail. Match it to the router's
+    // full ID and thread name to prove execution placement after handoff.
+    let install_marker = format!("installing delegated {transport} client fd");
+    tokio::time::timeout(TLS_TEST_TIMEOUT, async {
+        loop {
+            let logs = harness.server().stdout_plain();
+            let mut counts = vec![0; shard_count];
+            let mut installed = HashSet::new();
+            for line in logs.lines().filter(|line| 
line.contains(&install_marker)) {
+                let fields: Vec<_> = line.split_whitespace().collect();
+                let full_id: u128 = fields
+                    .iter()
+                    .find_map(|field| field.strip_prefix("client_id="))
+                    .expect("install log must carry client_id")
+                    .parse()
+                    .unwrap();
+                let wire_id = u32::try_from(full_id & 
u128::from(u32::MAX)).unwrap();
+                if !client_ids.contains(&wire_id) {
+                    continue;
+                }
+                let owner = usize::try_from(full_id >> 112).unwrap();
+                assert!(owner < shard_count, "invalid owner: {line}");
+                assert!(
+                    fields.contains(&format!("shard={owner}").as_str()),
+                    "{line}"
+                );
+                assert!(
+                    fields.contains(&format!("shard-{owner}").as_str()),
+                    "{line}"
+                );
+                assert!(installed.insert(wire_id), "client installed twice: 
{line}");
+                counts[owner] += 1;
+            }
+            if installed == client_ids {
+                assert_eq!(counts, vec![clients.len() / shard_count; 
shard_count]);
+                break;
+            }
+            tokio::time::sleep(TLS_POLL_INTERVAL).await;
+        }
+    })
+    .await
+    .expect("every encrypted client must be installed on its owning shard 
thread");
+    client_ids
+}
+
+async fn stalled_tls_clients(harness: &TestHarness, shard_count: usize) -> 
Vec<TcpStream> {
+    let address = match harness.transport().unwrap() {
+        TransportProtocol::Tcp => harness.server().tcp_addr().unwrap(),
+        TransportProtocol::WebSocket => 
harness.server().websocket_addr().unwrap(),
+        transport => panic!("expected TCP-TLS or WSS, got {transport:?}"),
+    };
+    let mut clients = Vec::with_capacity(shard_count);
+    for _ in 0..shard_count {
+        clients.push(TcpStream::connect(address).await.unwrap());
+    }
+    for _ in 0..shard_count {
+        let mut invalid = TcpStream::connect(address).await.unwrap();
+        invalid.write_all(b"GET / HTTP/1.1\r\n\r\n").await.unwrap();
+    }
+    clients
+}
diff --git a/core/message_bus/src/client_listener/mod.rs 
b/core/message_bus/src/client_listener/mod.rs
index f0f739496..5e1bf80b5 100644
--- a/core/message_bus/src/client_listener/mod.rs
+++ b/core/message_bus/src/client_listener/mod.rs
@@ -24,12 +24,10 @@
 //!   work runs in the install path (per [`crate::installer`]) so a slow
 //!   peer cannot block the accept loop.
 //! - The `on_accepted` callback is responsible for minting a
-//!   `client_id`. The plain TCP and pre-upgrade WS paths route to the
-//!   owning shard via `shard::LifecycleFrame::ClientConnectionSetup` and
-//!   `ClientWsConnectionSetup` respectively. TCP-TLS, WSS, and QUIC
-//!   stay shard-0 terminal: their connection state is not serialisable
-//!   so the callback installs locally on shard 0 instead of shipping a
-//!   setup frame.
+//!   `client_id`. TCP, WS, TCP-TLS and WSS delegate the raw TCP fd to the
+//!   owning shard before any handshake. TLS configuration accompanies
+//!   TCP-TLS and WSS setup frames. QUIC installs locally on shard 0
+//!   because its connections share the endpoint's UDP socket.
 //!
 //! `bind` signature families:
 //!
@@ -102,7 +100,7 @@ pub mod ws;
 pub mod wss;
 
 /// Bind a TCP listener with `TCP_NODELAY` set, the shared shape used by
-/// the plain-TCP and WS pre-upgrade client listeners.
+/// the plain-TCP, TCP-TLS and WS pre-upgrade client listeners.
 ///
 /// Binding stays synchronous through `bind_reusable_tcp_listener` so shard
 /// startup does not depend on compio's `IORING_OP_BIND` and
@@ -141,7 +139,8 @@ mod tests {
     use server_common::executor::create_shard_executor;
     use socket2::SockRef;
 
-    use super::bind_nodelay_listener;
+    use super::{tcp, tcp_tls};
+    use crate::transports::tls::self_signed_for_loopback;
 
     #[test]
     fn 
given_a_shard_executor_when_binding_a_client_listener_should_preserve_nodelay() 
{
@@ -149,10 +148,20 @@ mod tests {
         runtime.block_on(async {
             let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0);
 
-            let (listener, bound_addr) = bind_nodelay_listener(addr).unwrap();
+            let (tcp_listener, tcp_addr) = tcp::bind(addr).unwrap();
+            let (tls_listener, _, tls_addr) =
+                tcp_tls::bind(addr, self_signed_for_loopback()).unwrap();
 
-            assert_ne!(bound_addr.port(), 0);
-            assert!(SockRef::from(&listener).tcp_nodelay().unwrap());
+            for (transport, listener, bound_addr) in [
+                ("TCP", tcp_listener, tcp_addr),
+                ("TCP-TLS", tls_listener, tls_addr),
+            ] {
+                assert_ne!(bound_addr.port(), 0);
+                assert!(
+                    SockRef::from(&listener).tcp_nodelay().unwrap(),
+                    "{transport} listener must disable Nagle"
+                );
+            }
         });
     }
 }
diff --git a/core/message_bus/src/client_listener/tcp_tls.rs 
b/core/message_bus/src/client_listener/tcp_tls.rs
index 28ed0248d..cb3007633 100644
--- a/core/message_bus/src/client_listener/tcp_tls.rs
+++ b/core/message_bus/src/client_listener/tcp_tls.rs
@@ -24,13 +24,9 @@
 //! ([`crate::installer::install_client_tcp_tls`]) so a slow or
 //! malicious peer cannot block subsequent accepts.
 //!
-//! TCP-TLS is shard-0 terminal. Pre-handshake the fd is plain TCP and
-//! could in principle be dup'd to another shard, but the receiving shard
-//! would then have to perform the rustls handshake against a config that
-//! lives on shard 0 — losing the point of the cross-shard handover.
-//! Post-handshake the rustls connection state holds per-record sequence
-//! numbers, key schedule, and write buffers tied to the local task, with
-//! no dupable plaintext fd.
+//! The coordinator delegates the raw fd and shared configuration before
+//! any TLS bytes are consumed. The destination shard creates all
+//! per-connection TLS state and owns subsequent encrypted I/O.
 //!
 //! The TLS plane structurally cannot preserve `Frozen<MESSAGE_ALIGN>`
 //! ownership: rustls's encrypt step copies plaintext bytes into the
@@ -38,8 +34,8 @@
 //! TCP does not carry over.
 
 use crate::AcceptedTlsClientFn;
+use crate::client_listener::bind_nodelay_listener;
 use crate::lifecycle::ShutdownToken;
-use crate::socket_opts::bind_reusable_tcp_listener;
 use crate::transports::tls::{TlsServerCredentials, 
install_default_crypto_provider};
 use compio::net::TcpListener;
 use futures::FutureExt;
@@ -67,8 +63,8 @@ use tracing::{debug, error, info};
 ///
 /// # Errors
 ///
-/// - [`IggyError::IoError`] if the rustls server config cannot be built
-///   from `credentials` (cert / key mismatch).
+/// - [`IggyError::IoError`] if building the TLS config, configuring
+///   `TCP_NODELAY`, or reading the bound address fails.
 /// - [`IggyError::CannotBindToSocket`] if the TCP bind fails.
 #[allow(clippy::future_not_send)]
 pub fn bind(
@@ -86,11 +82,7 @@ pub fn bind(
     // cannot enable it accidentally.
     cfg.max_early_data_size = 0;
 
-    let listener = bind_reusable_tcp_listener(addr)
-        .map_err(|_| IggyError::CannotBindToSocket(addr.to_string()))?;
-    let actual = listener
-        .local_addr()
-        .map_err(|e| IggyError::IoError(e.to_string()))?;
+    let (listener, actual) = bind_nodelay_listener(addr)?;
     Ok((listener, Arc::new(cfg), actual))
 }
 
@@ -99,9 +91,8 @@ pub fn bind(
 /// Each accepted [`compio::net::TcpStream`] is handed to `on_accepted`
 /// together with a clone of the shared [`Arc<rustls::ServerConfig>`].
 /// The callback owns the stream from that point on; production wiring
-/// routes through shard 0's coordinator (mints a `client_id`, builds
-/// the install context, calls
-/// [`crate::installer::install_client_tcp_tls`]).
+/// delegates the raw fd and configuration through shard 0's coordinator.
+/// The destination shard calls [`crate::installer::install_client_tcp_tls`].
 #[allow(clippy::future_not_send)]
 pub async fn run(
     listener: TcpListener,
diff --git a/core/message_bus/src/client_listener/wss.rs 
b/core/message_bus/src/client_listener/wss.rs
index f686c65f8..8308a4460 100644
--- a/core/message_bus/src/client_listener/wss.rs
+++ b/core/message_bus/src/client_listener/wss.rs
@@ -26,14 +26,9 @@
 //! [`crate::transports::wss::WssTransportConn`]'s `run` method) so a
 //! slow or malicious peer cannot block subsequent accepts.
 //!
-//! WSS is shard-0 terminal. The plain TCP fd that exists pre-handshake
-//! could in principle be dup'd to another shard, but the receiving
-//! shard would then have to perform both the TLS handshake and the WS
-//! upgrade against state that lives on shard 0 — losing the point of
-//! the cross-shard handover. Post-upgrade the rustls + tungstenite
-//! state machines hold per-record sequence numbers, key schedule,
-//! masking state, and write buffers tied to the local task, with no
-//! dupable plaintext fd.
+//! The coordinator delegates the raw fd and shared configuration before
+//! either handshake. The destination shard creates all TLS and WebSocket
+//! state and owns subsequent encrypted I/O.
 //!
 //! Like the TCP-TLS plane, WSS structurally cannot preserve
 //! `Frozen<MESSAGE_ALIGN>` ownership: rustls's encrypt step copies
@@ -90,9 +85,8 @@ pub fn bind(
 /// Each accepted [`compio::net::TcpStream`] is handed to `on_accepted`
 /// together with a clone of the shared [`Arc<rustls::ServerConfig>`].
 /// The callback owns the stream from that point on; production wiring
-/// routes through shard 0's coordinator (mints a `client_id`, builds
-/// the install context, calls
-/// [`crate::installer::install_client_wss`]).
+/// delegates the raw fd and configuration through shard 0's coordinator.
+/// The destination shard calls [`crate::installer::install_client_wss`].
 #[allow(clippy::future_not_send)]
 pub async fn run(
     listener: TcpListener,
diff --git a/core/message_bus/src/fd_transfer.rs 
b/core/message_bus/src/fd_transfer.rs
index 118a5c56e..8bfd1f205 100644
--- a/core/message_bus/src/fd_transfer.rs
+++ b/core/message_bus/src/fd_transfer.rs
@@ -17,8 +17,8 @@
 
 //! File descriptor transfer between shards.
 //!
-//! After shard 0 accepts or connects a TCP socket and completes the
-//! handshake, it calls [`dup_fd`] to create a second kernel reference
+//! After shard 0 accepts or connects a TCP socket, before any handshake,
+//! it calls [`dup_fd`] to create a second kernel reference
 //! to the same socket. The duplicated fd is wrapped in an owning
 //! [`DupedFd`] and sent to the target shard via the inter-shard channel.
 //! The target shard calls [`wrap_duped_fd`] to construct a compio
@@ -138,7 +138,9 @@ fn close_fd(fd: RawFd) {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use compio::io::AsyncRead;
     use compio::net::{TcpListener, TcpStream};
+    use std::time::Duration;
 
     #[compio::test]
     #[allow(clippy::future_not_send)]
@@ -167,26 +169,28 @@ mod tests {
     #[compio::test]
     #[allow(clippy::future_not_send)]
     async fn duped_fd_drops_close_underlying_fd() {
+        const CLOSE_TIMEOUT: Duration = Duration::from_secs(2);
         let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
         let addr = listener.local_addr().unwrap();
         let connect = TcpStream::connect(addr);
         let accept = listener.accept();
         let (client_res, accept_res) = futures::join!(connect, accept);
-        let (_server, _) = accept_res.unwrap();
+        let (mut server, _) = accept_res.unwrap();
         let client = client_res.unwrap();
 
         let duped = dup_fd(&client).expect("dup_fd failed");
-        let raw = duped.as_raw_fd();
+        drop(client);
         drop(duped);
 
-        // After drop the fd must be gone from this process' fd table.
-        // SAFETY: F_GETFD on a closed fd is defined and returns -1/EBADF.
-        let flags = unsafe { libc::fcntl(raw, libc::F_GETFD) };
-        assert_eq!(flags, -1, "fd must be closed after DupedFd drop");
+        // Closed fd numbers can be reused immediately by other test threads.
+        let result = compio::time::timeout(CLOSE_TIMEOUT, server.read(vec![0]))
+            .await
+            .expect("DupedFd drop must release the socket")
+            .0;
         assert_eq!(
-            io::Error::last_os_error().raw_os_error(),
-            Some(libc::EBADF),
-            "closed fd must report EBADF"
+            result.unwrap(),
+            0,
+            "peer must observe EOF after both socket handles close"
         );
     }
 }
diff --git a/core/message_bus/src/installer/common.rs 
b/core/message_bus/src/installer/common.rs
index 918c23ad5..9d0278b75 100644
--- a/core/message_bus/src/installer/common.rs
+++ b/core/message_bus/src/installer/common.rs
@@ -28,11 +28,8 @@ use std::time::{Duration, Instant};
 /// already told both tasks to skip post-loop cleanup. Closing the
 /// sender wakes the transport's writer; triggering the per-connection
 /// shutdown wakes the transport's reader off its `io_uring` read SQE
-/// without waiting for peer EOF. Awaiting both handles with
-/// `close_peer_timeout` budget guarantees neither task can outlive the
-/// race on a half-open socket. `compio::runtime::JoinHandle::drop`
-/// detaches, so letting the handles go out of scope would leak the
-/// tasks.
+/// without waiting for peer EOF. Awaiting both handles allows cooperative
+/// cleanup; dropping a timed-out handle cancels its task.
 ///
 /// Both handles share a single `timeout` budget: after the transport
 /// returns (or is cancelled) the dispatch only gets the remaining time.
diff --git a/core/message_bus/src/installer/mod.rs 
b/core/message_bus/src/installer/mod.rs
index 641fd74e6..7ea17868b 100644
--- a/core/message_bus/src/installer/mod.rs
+++ b/core/message_bus/src/installer/mod.rs
@@ -50,16 +50,16 @@ use crate::client_listener::RequestHandler;
 use crate::fd_transfer::{self, DupedFd};
 use crate::installer::conn_info::ClientConnMeta;
 use crate::replica::listener::MessageHandler;
-use crate::{ClientConnectionLostFn, IggyMessageBus, ReplicaHandshakeDoneFn};
+use crate::{
+    ClientConnectionLostFn, IggyMessageBus, ReplicaHandshakeDoneFn, 
SharedTlsServerConfig,
+};
 use std::rc::Rc;
 use tracing::warn;
 
 /// Operations a shard needs to perform on its local bus when the router
 /// receives an inter-shard connection-setup frame.
 ///
-/// The production implementation is on `Rc<IggyMessageBus>`. The simulator
-/// does not exercise this path; if it ever does, add a no-op impl on
-/// `SharedSimOutbox`.
+/// The production implementation is on `Rc<IggyMessageBus>`.
 pub trait ConnectionInstaller {
     /// Wrap a blind-delegated inbound replica fd into a `TcpStream` on
     /// the local compio runtime, run the acceptor handshake in a spawned
@@ -113,6 +113,26 @@ pub trait ConnectionInstaller {
     /// access via the LOGIN allowlist.
     fn install_client_ws_fd(&self, fd: DupedFd, meta: ClientConnMeta, 
on_request: RequestHandler);
 
+    /// Wrap an unhandshaken TCP-TLS fd on this runtime and install its
+    /// transport task. The task owns the handshake, I/O and cleanup.
+    fn install_client_tcp_tls_fd(
+        &self,
+        fd: DupedFd,
+        meta: ClientConnMeta,
+        config: SharedTlsServerConfig,
+        on_request: RequestHandler,
+    );
+
+    /// Install an unhandshaken WSS fd on this runtime. TLS and the WebSocket
+    /// upgrade share the existing `handshake_grace` deadline in the task.
+    fn install_client_wss_fd(
+        &self,
+        fd: DupedFd,
+        meta: ClientConnMeta,
+        config: SharedTlsServerConfig,
+        on_request: RequestHandler,
+    );
+
     /// Per-connection metadata stored at install time, or `None` if the client
     /// is not (or no longer) connected on this bus. Dispatch reads `peer_addr`
     /// + `transport` from it to seed the shard's session manager.
@@ -173,11 +193,7 @@ impl ConnectionInstaller for Rc<IggyMessageBus> {
             )
             .await;
             match outcome {
-                Ok(Ok(ws)) => {
-                    if !bus.is_shutting_down() {
-                        install_client_ws(&bus, meta, ws, on_request);
-                    }
-                }
+                Ok(Ok(ws)) => install_client_ws(&bus, meta, ws, on_request),
                 Ok(Err(e)) => {
                     warn!(client_id = meta.client_id, "WS upgrade failed: 
{e}");
                 }
@@ -193,6 +209,28 @@ impl ConnectionInstaller for Rc<IggyMessageBus> {
         self.track_background(handle);
     }
 
+    fn install_client_tcp_tls_fd(
+        &self,
+        fd: DupedFd,
+        meta: ClientConnMeta,
+        config: SharedTlsServerConfig,
+        on_request: RequestHandler,
+    ) {
+        let stream = fd_transfer::wrap_duped_fd(fd);
+        install_client_tcp_tls(self, meta, stream, config, on_request);
+    }
+
+    fn install_client_wss_fd(
+        &self,
+        fd: DupedFd,
+        meta: ClientConnMeta,
+        config: SharedTlsServerConfig,
+        on_request: RequestHandler,
+    ) {
+        let stream = fd_transfer::wrap_duped_fd(fd);
+        install_client_wss(self, meta, stream, config, on_request);
+    }
+
     fn client_meta(&self, client_id: u128) -> Option<Rc<ClientConnMeta>> {
         IggyMessageBus::client_meta(self, client_id)
     }
diff --git a/core/message_bus/src/installer/replica.rs 
b/core/message_bus/src/installer/replica.rs
index bf8a344b7..5bb4556fb 100644
--- a/core/message_bus/src/installer/replica.rs
+++ b/core/message_bus/src/installer/replica.rs
@@ -369,8 +369,6 @@ pub fn install_replica_conn<C: TransportConn>(
     // `notify_connection_lost` stands down whenever a live registry entry
     // exists - but the loser also drives `replica_dispatch_loop`, which
     // must never hand the winner's replica id to `on_message`.
-    // `compio::runtime::JoinHandle::drop` does not cancel the spawned
-    // task, so we have to tell the tasks to stand down in-band.
     let install_aborted = Rc::new(Cell::new(false));
 
     // Generation token published by the registry on a successful insert.
@@ -575,15 +573,9 @@ pub fn install_replica_conn<C: TransportConn>(
             install_aborted.set(true);
             let _ = bus.clear_replica_owned(peer_id);
             warn!(replica = peer_id, "replica registry insert raced");
-            // `drain_rejected_registration` triggers the per-connection
-            // shutdown (returned with `rejected`) to wake the transport
-            // off its `io_uring` read SQE, then awaits transport /
-            // dispatch handles. `compio::runtime::JoinHandle::drop` only
-            // detaches, so without this drain the reader would outlive
-            // the race on a half-open socket until peer EOF. Hand the
-            // handle to `track_background` so `IggyMessageBus::shutdown`
-            // awaits the drain before returning - `.detach()` would
-            // orphan it and leak the half-closed socket across shutdown.
+            // Allow bounded cooperative cleanup of the losing connection.
+            // Track the drain so bus shutdown awaits it; dropping its
+            // handle would cancel that cleanup.
             let drain_handle =
                 compio::runtime::spawn(drain_rejected_registration(rejected, 
close_peer_timeout));
             bus.track_background(drain_handle);
diff --git a/core/message_bus/src/installer/tcp.rs 
b/core/message_bus/src/installer/tcp.rs
index 67bfc091e..b9908bae8 100644
--- a/core/message_bus/src/installer/tcp.rs
+++ b/core/message_bus/src/installer/tcp.rs
@@ -61,6 +61,8 @@ pub fn install_client_tcp(
 /// Install a pre-wrapped client connection on the bus. Generic over
 /// [`TransportConn`]; plane-symmetric with
 /// [`super::replica::install_replica_conn`].
+///
+/// Connections arriving after shutdown starts are dropped before registration.
 #[allow(clippy::future_not_send, clippy::too_many_lines)]
 pub fn install_client_conn<C: TransportConn>(
     bus: &Rc<IggyMessageBus>,
@@ -68,6 +70,9 @@ pub fn install_client_conn<C: TransportConn>(
     conn: C,
     on_request: RequestHandler,
 ) {
+    if bus.is_shutting_down() {
+        return;
+    }
     let client_id = meta.client_id;
     let (tx, rx) = async_channel::bounded(bus.config().client_queue_capacity);
     let (in_tx, in_rx) =
diff --git a/core/message_bus/src/installer/tcp_tls.rs 
b/core/message_bus/src/installer/tcp_tls.rs
index 3497aac79..5db4c84df 100644
--- a/core/message_bus/src/installer/tcp_tls.rs
+++ b/core/message_bus/src/installer/tcp_tls.rs
@@ -43,12 +43,9 @@ use tracing::warn;
 /// to accepted sockets, so toggling here is required. `SO_KEEPALIVE`
 /// is intentionally NOT set; see `socket_opts`.
 ///
-/// TCP-TLS is shard-0 terminal: the rustls connection state machine
-/// is non-serialisable and tied to the local task; pre-handshake the
-/// fd is plain TCP and could in principle be dup'd to another shard,
-/// but the receiving shard would then have to re-handshake against
-/// shard-0-resident key material — losing the point of the cross-shard
-/// handover.
+/// Called on the destination shard after raw-fd delegation. The shared
+/// configuration crosses shards; per-connection TLS state stays on this
+/// runtime for the lifetime of the connection.
 #[allow(clippy::future_not_send)]
 pub fn install_client_tcp_tls(
     bus: &Rc<IggyMessageBus>,
diff --git a/core/message_bus/src/installer/wss.rs 
b/core/message_bus/src/installer/wss.rs
index d65f66fd3..25f6ee5d9 100644
--- a/core/message_bus/src/installer/wss.rs
+++ b/core/message_bus/src/installer/wss.rs
@@ -43,9 +43,8 @@ use tracing::warn;
 /// [`super::tcp_tls::install_client_tcp_tls`]. `SO_KEEPALIVE` is
 /// intentionally NOT set; see `socket_opts`.
 ///
-/// WSS is shard-0 terminal for the same reasons as the TCP-TLS plane;
-/// see [`super::tcp_tls::install_client_tcp_tls`] for the rustls
-/// non-serialisability argument.
+/// Called on the destination shard after raw-fd delegation. Both TLS and
+/// WebSocket state are created and driven exclusively on this runtime.
 #[allow(clippy::future_not_send)]
 pub fn install_client_wss(
     bus: &Rc<IggyMessageBus>,
diff --git a/core/message_bus/src/lib.rs b/core/message_bus/src/lib.rs
index d8038b612..dc2348daa 100644
--- a/core/message_bus/src/lib.rs
+++ b/core/message_bus/src/lib.rs
@@ -57,10 +57,10 @@
 //!   (their `Frozen<MESSAGE_ALIGN>` fragments flattened, chunked at
 //!   `IOV_MAX`) into `write_vectored_all`. Don't introduce per-message
 //!   syscalls or per-message encryption on the plaintext TCP plane.
-//! - fd-delegation ([`fd_transfer`]) is TCP-only. TLS / QUIC
-//!   connections have no dupable plaintext fd, so shard 0 terminates
-//!   and forwards `Frozen<MESSAGE_ALIGN>` over the existing
-//!   inter-shard crossfire channel.
+//! - fd-delegation ([`fd_transfer`]) transfers raw TCP sockets for TCP,
+//!   WS, TCP-TLS and WSS before handshakes. The destination shard owns
+//!   handshake state and I/O. QUIC client callbacks install on shard 0
+//!   through its shared UDP endpoint.
 //! - 0-RTT stays disabled by default on any future QUIC path. Per-
 //!   command opt-in requires a checked-in idempotence audit.
 //!
@@ -403,42 +403,26 @@ pub type AcceptedQuicClientFn = std::rc::Rc<dyn 
Fn(AcceptedQuicConn)>;
 /// requires a dupable plaintext fd) stays well-defined.
 pub type AcceptedWsClientFn = std::rc::Rc<dyn Fn(compio::net::TcpStream)>;
 
+/// Listener TLS configuration shared with the shard owning each connection.
+/// Per-connection TLS state is created only on the destination runtime.
+pub type SharedTlsServerConfig = std::sync::Arc<rustls::ServerConfig>;
+
 /// Callback invoked on every accepted SDK TCP-TLS client connection.
 ///
 /// Fires after shard 0's TCP-TLS listener accepts a raw TCP socket.
-/// Neither the rustls handshake nor any application-layer work has run
-/// yet — the listener stays cheap so a slow handshake on one peer cannot
-/// block subsequent accepts. The callback receives the raw stream plus
-/// a clone of the shared [`std::sync::Arc<rustls::ServerConfig>`] built
-/// at bind time, mints a `client_id`, and calls
-/// [`installer::install_client_tcp_tls`]; the install path drives the
-/// rustls handshake on its own task before forwarding the connection to
-/// [`installer::install_client_conn`].
-///
-/// TCP-TLS stays shard-0 terminal: rustls's connection state machine
-/// is tied to the local task and not serialisable, and the
-/// pre-handshake fd would have to re-handshake on the receiving shard,
-/// losing the point of fd-delegation.
-pub type AcceptedTlsClientFn =
-    std::rc::Rc<dyn Fn(compio::net::TcpStream, 
std::sync::Arc<rustls::ServerConfig>)>;
+/// The callback delegates the socket and its listener's configuration
+/// before any TLS bytes are consumed. The destination shard runs
+/// [`installer::install_client_tcp_tls`], owning the handshake and all I/O.
+pub type AcceptedTlsClientFn = std::rc::Rc<dyn Fn(compio::net::TcpStream, 
SharedTlsServerConfig)>;
 
 /// Callback invoked on every accepted SDK WSS (WebSocket-over-TLS)
 /// client connection.
 ///
-/// Fires after shard 0's WSS listener accepts a raw TCP socket. Neither
-/// the rustls handshake nor the WebSocket HTTP-Upgrade has run yet — the
-/// listener stays cheap so neither handshake on one peer can block
-/// subsequent accepts. The callback receives the raw stream plus a clone
-/// of the shared [`std::sync::Arc<rustls::ServerConfig>`] built at bind
-/// time, mints a `client_id`, and calls
-/// [`installer::install_client_wss`]; the install path drives
-/// both handshakes on its own task before forwarding the connection to
-/// [`installer::install_client_conn`].
-///
-/// No subprotocol negotiation is performed. WSS stays shard-0 terminal
-/// for the same reasons as the TCP-TLS plane.
-pub type AcceptedWssClientFn =
-    std::rc::Rc<dyn Fn(compio::net::TcpStream, 
std::sync::Arc<rustls::ServerConfig>)>;
+/// Fires before either the TLS handshake or WebSocket upgrade. The callback
+/// delegates the raw socket and its listener's configuration to the owning
+/// shard, where [`installer::install_client_wss`] runs both handshakes and
+/// subsequent I/O. No subprotocol negotiation is performed.
+pub type AcceptedWssClientFn = std::rc::Rc<dyn Fn(compio::net::TcpStream, 
SharedTlsServerConfig)>;
 
 /// Notifier fired when a delegated replica connection dies.
 ///
@@ -1224,6 +1208,8 @@ impl IggyMessageBus {
 
         let remaining = 
deadline.saturating_duration_since(std::time::Instant::now());
         let clients_outcome = self.clients.drain(remaining).await;
+        // Connection tasks skip per-client cleanup during bus shutdown.
+        self.client_meta.borrow_mut().clear();
         let remaining = 
deadline.saturating_duration_since(std::time::Instant::now());
         let replicas_outcome = self.replicas.drain(remaining).await;
 
diff --git a/core/message_bus/src/lifecycle/connection_registry.rs 
b/core/message_bus/src/lifecycle/connection_registry.rs
index 24c709742..571e615eb 100644
--- a/core/message_bus/src/lifecycle/connection_registry.rs
+++ b/core/message_bus/src/lifecycle/connection_registry.rs
@@ -90,12 +90,11 @@ pub type BusReceiver = async_channel::Receiver<BusMessage>;
 ///
 /// Exposes the writer and reader [`JoinHandle`]s plus the per-connection
 /// [`Shutdown`] so the loser can explicitly drain (or force-cancel on
-/// deadline) the orphan tasks rather than relying on them to self-exit via
-/// `install_aborted`. `compio::runtime::JoinHandle::drop` detaches; without
-/// this the loser would leak the handles and a reader looping on
-/// `framing::read_message` could outlive the race indefinitely on a
-/// half-open socket. Triggering the [`Shutdown`] wakes the reader off its
-/// `io_uring` read SQE without waiting for peer EOF.
+/// deadline) the orphan tasks. `install_aborted` prevents them from
+/// dispatching frames or cleaning up the winning entry. Dropping a
+/// [`JoinHandle`] cancels its task; explicitly draining allows cooperative
+/// shutdown before the deadline. Triggering [`Shutdown`] wakes the reader
+/// without waiting for peer EOF.
 ///
 /// [`Shutdown`]: crate::lifecycle::Shutdown
 #[derive(Debug)]
diff --git a/core/message_bus/src/replica/io.rs 
b/core/message_bus/src/replica/io.rs
index 9368ef5a7..0ae9e6874 100644
--- a/core/message_bus/src/replica/io.rs
+++ b/core/message_bus/src/replica/io.rs
@@ -18,25 +18,20 @@
 //! Shard-0 listener bootstrap.
 //!
 //! Every shard on a node instantiates its own `Rc<IggyMessageBus>` on its
-//! own compio runtime, but only shard 0 terminates inbound traffic: it
-//! binds the replica listener, the plain-TCP client listener, and
-//! optionally the WS, QUIC, TCP-TLS, and WSS client listeners. It also
-//! dials higher-id peers for the replica plane. Each accepted or
+//! own compio runtime. Shard 0 binds the replica and client listeners
+//! and dials higher-id peers for the replica plane. Each accepted or
 //! dialed connection is handed to the delegate callback supplied by
 //! the caller (typically wrapping `shard::coordinator::ShardZeroCoordinator`).
-//! Replica, client-TCP, and pre-upgrade WS callbacks duplicate the raw
-//! fd and ship it to the owning shard via the inter-shard `ShardFrame`
-//! channel; replica delegation is blind (no byte is read on shard 0 -
-//! the owning shard runs the handshake, see
-//! [`crate::replica::handshake`]). TCP-TLS, WSS, and QUIC CLIENT
-//! callbacks install locally on shard 0: their handshake state
-//! materialises before delegation could happen, and post-handshake
-//! rustls / QUIC state is not serialisable.
+//! Replica, TCP, WS, TCP-TLS and WSS callbacks duplicate the raw fd and
+//! ship it to the owning shard before any bytes are read. TLS configuration
+//! travels with TCP-TLS and WSS sockets. The owning shard runs handshakes
+//! and subsequent I/O; see [`crate::replica::handshake`] for replica
+//! handshakes. QUIC client callbacks install locally on shard 0 through
+//! its shared UDP endpoint.
 //!
-//! Non-zero shards early-return `Ok(None)`; the launcher calls this helper
-//! unconditionally per shard, so non-zero shards just have no listener
-//! binding and rely on `send_to_*` slow-path forwarding to reach the
-//! owning shard.
+//! Non-zero shards early-return `Ok(None)` and receive delegated sockets
+//! through their inboxes. `send_to_*` forwards traffic when another shard
+//! owns the connection.
 
 use std::net::SocketAddr;
 use std::rc::Rc;
@@ -135,17 +130,13 @@ pub fn assert_listen_addrs_distinct(
 /// Optionally bind WS, QUIC, TCP-TLS, and WSS client listeners
 /// alongside. Non-zero shards early-return `Ok(None)`.
 ///
-/// Each accepted / dialed connection is handed to the supplied
-/// delegate callback. The replica callbacks (`on_accepted_replica` for
-/// blind inbound delegation, `on_dialed_replica` for outbound), the
-/// client-TCP callback (`on_accepted_client`), and the WS callback
-/// (`on_accepted_ws_client`) are responsible for the dup-fd +
-/// inter-shard send. The QUIC, TCP-TLS, and WSS callbacks
-/// (`on_accepted_quic_client` / `on_accepted_tcp_tls_client` /
-/// `on_accepted_wss_client`) install locally on shard 0; those CLIENT
-/// planes have no cross-shard handover (QUIC by transport design,
-/// TLS-family because the handshake completes before delegation could
-/// happen and the resulting rustls state is not serialisable).
+/// Each accepted or dialed connection is handed to its supplied callback.
+/// Replica, TCP, WS, TCP-TLS and WSS callbacks duplicate the raw fd and
+/// send it to the owning shard before any handshake. TCP-TLS and WSS
+/// also transfer the listener's shared TLS configuration. The owning
+/// shard supplies its handlers and performs handshakes and I/O.
+/// The QUIC client callback installs locally on shard 0 through its
+/// shared UDP endpoint.
 ///
 /// `ws_listen_addr` / `on_accepted_ws_client` are paired: if either is
 /// `Some`, both must be. Same for the QUIC trio
diff --git a/core/message_bus/src/transports/tcp_tls.rs 
b/core/message_bus/src/transports/tcp_tls.rs
index c3fa5ac21..c0155991a 100644
--- a/core/message_bus/src/transports/tcp_tls.rs
+++ b/core/message_bus/src/transports/tcp_tls.rs
@@ -218,7 +218,11 @@ impl TransportConn for TcpTlsTransportConn {
         let mut tls = match self.state {
             ConnState::Established(tls) => *tls,
             ConnState::Pending { stream, role } => {
-                match compio::time::timeout(handshake_grace, handshake(role, 
stream)).await {
+                let outcome = futures::select_biased! {
+                    () = ctx.shutdown.wait().fuse() => return,
+                    outcome = compio::time::timeout(handshake_grace, 
handshake(role, stream)).fuse() => outcome,
+                };
+                match outcome {
                     Ok(Ok(s)) => s,
                     Ok(Err(e)) => {
                         warn!(
diff --git a/core/message_bus/src/transports/wss.rs 
b/core/message_bus/src/transports/wss.rs
index 20168d008..b28b93ebf 100644
--- a/core/message_bus/src/transports/wss.rs
+++ b/core/message_bus/src/transports/wss.rs
@@ -174,12 +174,14 @@ impl TransportConn for WssTransportConn {
         // both inner handshake futures are large (rustls + tungstenite
         // state machines) and would otherwise push `run`'s state past
         // the `clippy::large_futures` threshold.
-        let tls_stream = match compio::time::timeout(
-            handshake_grace,
-            Box::pin(tls_handshake(role, self.stream)),
-        )
-        .await
-        {
+        let tls_outcome = futures::select_biased! {
+            () = ctx.shutdown.wait().fuse() => return,
+            outcome = compio::time::timeout(
+                handshake_grace,
+                Box::pin(tls_handshake(role, self.stream)),
+            ).fuse() => outcome,
+        };
+        let tls_stream = match tls_outcome {
             Ok(Ok(s)) => s,
             Ok(Err(e)) => {
                 warn!(
@@ -212,12 +214,14 @@ impl TransportConn for WssTransportConn {
             );
             return;
         }
-        let mut ws = match compio::time::timeout(
-            remaining,
-            Box::pin(ws_handshake(tls_stream, is_server, &peer, ws_config)),
-        )
-        .await
-        {
+        let upgrade_outcome = futures::select_biased! {
+            () = ctx.shutdown.wait().fuse() => return,
+            outcome = compio::time::timeout(
+                remaining,
+                Box::pin(ws_handshake(tls_stream, is_server, &peer, 
ws_config)),
+            ).fuse() => outcome,
+        };
+        let mut ws = match upgrade_outcome {
             Ok(Ok(s)) => s,
             Ok(Err(e)) => {
                 warn!(
diff --git a/core/message_bus/tests/common/mod.rs 
b/core/message_bus/tests/common/mod.rs
index b5630b764..da16e8dd9 100644
--- a/core/message_bus/tests/common/mod.rs
+++ b/core/message_bus/tests/common/mod.rs
@@ -26,6 +26,8 @@
 
 #![allow(dead_code)] // each test binary uses a subset
 
+use compio::io::{AsyncRead, AsyncWriteExt};
+use compio::net::TcpStream;
 use iggy_binary_protocol::{Command, GenericHeader, HEADER_SIZE};
 use message_bus::ConnectionInstaller;
 use message_bus::client_listener::RequestHandler;
@@ -46,6 +48,10 @@ use std::cell::Cell;
 use std::net::SocketAddr;
 use std::rc::Rc;
 use std::sync::Arc;
+use std::time::Duration;
+
+const CLIENT_CLEANUP_TIMEOUT: Duration = Duration::from_secs(2);
+const CLIENT_POLL_INTERVAL: Duration = Duration::from_millis(10);
 
 /// Build a stub [`ClientConnMeta`] for tests that don't care about
 /// peer addr / transport details. Uses `127.0.0.1:0` and the given
@@ -263,9 +269,9 @@ pub fn install_ws_clients_locally(
 }
 
 /// Build an [`AcceptedTlsClientFn`] that mints a local client id and
-/// installs the accepted TCP-TLS stream directly on the given bus. The
-/// install path drives the rustls handshake on its own task, mirroring
-/// the production shard-0 coordinator.
+/// duplicates the raw TCP-TLS fd into the owning-shard installer. This
+/// exercises fd wrapping locally; server integration tests cover routing
+/// from the production accept callback to a different shard.
 #[must_use]
 pub fn install_tls_clients_locally(
     bus: Rc<IggyMessageBus>,
@@ -278,12 +284,14 @@ pub fn install_tls_clients_locally(
         counter.set(seq.wrapping_add(1));
         let client_id = (shard_id << 112) | seq;
         let meta = test_client_meta(client_id, ClientTransportKind::TcpTls);
-        installer::install_client_tcp_tls(&bus, meta, stream, config, 
on_request.clone());
+        let fd = fd_transfer::dup_fd(&stream).expect("dup_fd");
+        drop(stream);
+        bus.install_client_tcp_tls_fd(fd, meta, config, on_request.clone());
     })
 }
 
 /// Build an [`AcceptedWssClientFn`] that mints a local client id and
-/// installs the accepted WSS stream directly on the given bus. The
+/// duplicates the raw WSS fd into the owning-shard installer. The
 /// install path drives both the rustls handshake and the WS HTTP-Upgrade
 /// inside the transport's `run` body.
 #[must_use]
@@ -298,6 +306,97 @@ pub fn install_wss_clients_locally(
         counter.set(seq.wrapping_add(1));
         let client_id = (shard_id << 112) | seq;
         let meta = test_client_meta(client_id, ClientTransportKind::Wss);
-        installer::install_client_wss(&bus, meta, stream, config, 
on_request.clone());
+        let fd = fd_transfer::dup_fd(&stream).expect("dup_fd");
+        drop(stream);
+        bus.install_client_wss_fd(fd, meta, config, on_request.clone());
     })
 }
+
+#[allow(clippy::future_not_send)]
+pub async fn wait_for_client_count(bus: &IggyMessageBus, count: usize) {
+    compio::time::timeout(CLIENT_CLEANUP_TIMEOUT, async {
+        while bus.clients().len() != count {
+            compio::time::sleep(CLIENT_POLL_INTERVAL).await;
+        }
+    })
+    .await
+    .unwrap_or_else(|_| panic!("expected {count} clients, got {}", 
bus.clients().len()));
+}
+
+#[allow(clippy::future_not_send)]
+pub async fn assert_failed_tls_installs_are_removed(transport: 
ClientTransportKind) {
+    const OWNER: u16 = 1;
+    const FIRST_CLIENT_ID: u128 = (OWNER as u128) << 112 | 1;
+    const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(1);
+    let bus = Rc::new(IggyMessageBus::new(OWNER));
+    let request_seen = Rc::new(Cell::new(false));
+    let seen_by_handler = Rc::clone(&request_seen);
+    let on_request: RequestHandler = Rc::new(move |_, _| 
seen_by_handler.set(true));
+    let credentials = self_signed_for_loopback();
+    let (listener, config, address, accepted) = match transport {
+        ClientTransportKind::TcpTls => {
+            let (listener, config, address) =
+                message_bus::client_listener::tcp_tls::bind(loopback(), 
credentials).unwrap();
+            (
+                listener,
+                config,
+                address,
+                install_tls_clients_locally(Rc::clone(&bus), on_request),
+            )
+        }
+        ClientTransportKind::Wss => {
+            let (listener, config, address) =
+                message_bus::client_listener::wss::bind(loopback(), 
credentials).unwrap();
+            (
+                listener,
+                config,
+                address,
+                install_wss_clients_locally(Rc::clone(&bus), on_request),
+            )
+        }
+        _ => panic!("expected encrypted TCP transport"),
+    };
+
+    let mut invalid = TcpStream::connect(address).await.unwrap();
+    accepted(listener.accept().await.unwrap().0, Arc::clone(&config));
+    assert!(bus.client_meta(FIRST_CLIENT_ID).is_some());
+    invalid
+        .write_all(b"GET / HTTP/1.1\r\n\r\n")
+        .await
+        .0
+        .unwrap();
+    wait_for_client_count(&bus, 0).await;
+    assert!(bus.client_meta(FIRST_CLIENT_ID).is_none());
+
+    let stalled = TcpStream::connect(address).await.unwrap();
+    accepted(listener.accept().await.unwrap().0, Arc::clone(&config));
+    assert!(bus.client_meta(FIRST_CLIENT_ID + 1).is_some());
+    // Poll the spawned transport so shutdown interrupts a pending handshake.
+    compio::time::sleep(CLIENT_POLL_INTERVAL).await;
+    assert_eq!(
+        bus.shutdown(SHUTDOWN_TIMEOUT).await.force,
+        0,
+        "stalled {transport:?} handshake must observe shutdown without 
force-cancellation"
+    );
+    assert!(bus.clients().is_empty());
+    assert!(bus.client_meta(FIRST_CLIENT_ID + 1).is_none());
+
+    let late = TcpStream::connect(address).await.unwrap();
+    accepted(listener.accept().await.unwrap().0, config);
+    assert!(
+        bus.clients().is_empty(),
+        "setup after shutdown must be rejected"
+    );
+    assert!(bus.client_meta(FIRST_CLIENT_ID + 2).is_none());
+    for mut peer in [stalled, late] {
+        let result = compio::time::timeout(CLIENT_CLEANUP_TIMEOUT, 
peer.read(vec![0]))
+            .await
+            .expect("shutdown must close pending and late handshake sockets")
+            .0;
+        assert_eq!(result.unwrap(), 0);
+    }
+    assert!(
+        !request_seen.get(),
+        "failed handshakes must not dispatch requests"
+    );
+}
diff --git a/core/message_bus/tests/tcp_tls_client_listener.rs 
b/core/message_bus/tests/tcp_tls_client_listener.rs
index d477d1cd4..1a826e4be 100644
--- a/core/message_bus/tests/tcp_tls_client_listener.rs
+++ b/core/message_bus/tests/tcp_tls_client_listener.rs
@@ -182,3 +182,8 @@ async fn slow_tls_handshake_evicts_registry() {
         "graceful shutdown should not force-cancel"
     );
 }
+
+#[compio::test]
+async fn failed_and_interrupted_tls_handshakes_release_delegated_connections() 
{
+    
common::assert_failed_tls_installs_are_removed(message_bus::ClientTransportKind::TcpTls).await;
+}
diff --git a/core/message_bus/tests/wss_client_listener.rs 
b/core/message_bus/tests/wss_client_listener.rs
index f280d8ebf..b65ea5522 100644
--- a/core/message_bus/tests/wss_client_listener.rs
+++ b/core/message_bus/tests/wss_client_listener.rs
@@ -19,7 +19,9 @@ mod common;
 
 use async_channel::bounded;
 use common::{header_only, install_wss_clients_locally, loopback};
+use compio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
 use compio::net::TcpStream;
+use compio::tls::TlsConnector;
 use iggy_binary_protocol::Command;
 use iggy_binary_protocol::GenericHeader;
 use message_bus::BusMessage;
@@ -32,6 +34,7 @@ use message_bus::{FusedShutdown, IggyMessageBus, MessageBus, 
MessageBusConfig, S
 use rustls::RootCertStore;
 use rustls::pki_types::ServerName;
 use server_common::Message;
+use std::cell::Cell;
 use std::rc::Rc;
 use std::sync::Arc;
 use std::time::{Duration, Instant};
@@ -170,3 +173,108 @@ async fn slow_handshake_evicts_registry() {
         "graceful shutdown should not force-cancel"
     );
 }
+
+#[compio::test]
+async fn failed_and_interrupted_wss_handshakes_release_delegated_connections() 
{
+    
common::assert_failed_tls_installs_are_removed(message_bus::ClientTransportKind::Wss).await;
+}
+
+#[compio::test]
+async fn 
stalled_rejected_and_interrupted_websocket_upgrades_release_delegated_connections()
 {
+    const OWNER: u16 = 1;
+    const FIRST_CLIENT_ID: u128 = (OWNER as u128) << 112 | 1;
+    const HANDSHAKE_GRACE: Duration = Duration::from_secs(2);
+    const TLS_DELAY: Duration = Duration::from_millis(1_200);
+    const DEADLINE_MARGIN: Duration = Duration::from_millis(400);
+    const IO_TIMEOUT: Duration = Duration::from_secs(3);
+    const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(1);
+    const UPGRADE_WAIT: Duration = Duration::from_millis(10);
+
+    install_default_crypto_provider();
+    let bus = Rc::new(IggyMessageBus::with_tunables(
+        OWNER,
+        MessageBusConfig {
+            handshake_grace: HANDSHAKE_GRACE,
+            ..MessageBusConfig::default()
+        },
+    ));
+    let credentials = self_signed_for_loopback();
+    let mut roots = RootCertStore::empty();
+    for cert in &credentials.cert_chain {
+        roots.add(cert.clone()).unwrap();
+    }
+    let connector = TlsConnector::from(Arc::new(
+        rustls::ClientConfig::builder()
+            .with_root_certificates(roots)
+            .with_no_client_auth(),
+    ));
+    let (listener, config, address) = bind(loopback(), credentials).unwrap();
+    let request_seen = Rc::new(Cell::new(false));
+    let seen_by_handler = Rc::clone(&request_seen);
+    let on_request: RequestHandler = Rc::new(move |_, _| 
seen_by_handler.set(true));
+    let accepted = install_wss_clients_locally(Rc::clone(&bus), on_request);
+
+    let tcp = TcpStream::connect(address).await.unwrap();
+    accepted(listener.accept().await.unwrap().0, Arc::clone(&config));
+    let installed_at = Instant::now();
+    compio::time::sleep(TLS_DELAY).await;
+    let mut tls = compio::time::timeout(IO_TIMEOUT, 
connector.connect("localhost", tcp))
+        .await
+        .unwrap()
+        .unwrap();
+    common::wait_for_client_count(&bus, 0).await;
+    assert!(
+        installed_at.elapsed() < HANDSHAKE_GRACE + DEADLINE_MARGIN,
+        "TLS and WebSocket must share one deadline, elapsed {:?}",
+        installed_at.elapsed()
+    );
+    assert!(bus.client_meta(FIRST_CLIENT_ID).is_none());
+    let closed = compio::time::timeout(IO_TIMEOUT, tls.read(vec![0]))
+        .await
+        .unwrap()
+        .0;
+    assert!(closed.is_err() || closed.unwrap() == 0);
+
+    let tcp = TcpStream::connect(address).await.unwrap();
+    accepted(listener.accept().await.unwrap().0, Arc::clone(&config));
+    let mut tls = compio::time::timeout(IO_TIMEOUT, 
connector.connect("localhost", tcp))
+        .await
+        .unwrap()
+        .unwrap();
+    assert!(bus.client_meta(FIRST_CLIENT_ID + 1).is_some());
+    tls.write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n")
+        .await
+        .0
+        .unwrap();
+    tls.flush().await.unwrap();
+    common::wait_for_client_count(&bus, 0).await;
+    assert!(bus.client_meta(FIRST_CLIENT_ID + 1).is_none());
+
+    let tcp = TcpStream::connect(address).await.unwrap();
+    accepted(listener.accept().await.unwrap().0, config);
+    let mut tls = compio::time::timeout(IO_TIMEOUT, 
connector.connect("localhost", tcp))
+        .await
+        .unwrap()
+        .unwrap();
+    tls.write_all(b"GET / HTTP/1.1\r\n").await.0.unwrap();
+    tls.flush().await.unwrap();
+    // Let the server finish TLS and wait for the remaining upgrade headers.
+    compio::time::sleep(UPGRADE_WAIT).await;
+    assert!(bus.client_meta(FIRST_CLIENT_ID + 2).is_some());
+    assert_eq!(
+        bus.shutdown(SHUTDOWN_TIMEOUT).await.force,
+        0,
+        "pending WebSocket upgrade must observe shutdown without 
force-cancellation"
+    );
+    assert!(bus.clients().is_empty());
+    assert!(bus.client_meta(FIRST_CLIENT_ID + 2).is_none());
+    let closed = compio::time::timeout(IO_TIMEOUT, tls.read(vec![0]))
+        .await
+        .unwrap()
+        .0;
+    assert!(closed.is_err() || closed.unwrap() == 0);
+    assert!(
+        !request_seen.get(),
+        "failed upgrades must not dispatch requests"
+    );
+}
diff --git a/core/server/src/boot/listeners.rs 
b/core/server/src/boot/listeners.rs
index 9ffb8c2ee..9967376d1 100644
--- a/core/server/src/boot/listeners.rs
+++ b/core/server/src/boot/listeners.rs
@@ -403,11 +403,10 @@ pub(in crate::boot) fn make_replica_delegation_fns(
     (accepted, dialed)
 }
 
-/// Shard-0 client accept callbacks. TCP and WS clients are delegated via
-/// the coordinator (round-robin to peer shards); QUIC and TCP-TLS install
-/// locally on shard 0 because their per-connection state is not portable
-/// across shards (`compio_quic` endpoint binds one UDP socket; rustls TLS
-/// state ties to the post-handshake reactor).
+/// Shard-0 client accept callbacks. TCP, WS, TCP-TLS and WSS delegate
+/// raw sockets through the coordinator before any handshake. The
+/// destination shard supplies its handler and owns handshakes and I/O.
+/// The QUIC callback installs locally through shard 0's UDP endpoint.
 // ws/wss bindings intentionally mirror the transport names (same convention as
 // `replica_io::start_on_shard_zero`).
 #[allow(clippy::similar_names)]
@@ -417,11 +416,7 @@ pub(in crate::boot) fn make_shard_zero_client_accept_fns(
     on_request: RequestHandler,
 ) -> LocalClientAcceptFns {
     let quic_bus = Rc::clone(bus);
-    let tcp_tls_bus = Rc::clone(bus);
-    let wss_bus = Rc::clone(bus);
-    let quic_request = on_request.clone();
-    let wss_request = on_request.clone();
-    let tcp_tls_request = on_request;
+    let quic_request = on_request;
 
     let tcp_coord = Rc::clone(&coord);
     let tcp = Rc::new(move |stream| match tcp_coord.delegate_client(stream) {
@@ -435,9 +430,9 @@ pub(in crate::boot) fn make_shard_zero_client_accept_fns(
         Err(error) => warn!(error = ?error, "delegate_ws_client failed; 
dropping WS client"),
     });
 
-    // QUIC and TCP-TLS terminate locally on shard 0 but mint their client
+    // QUIC terminates locally on shard 0 but mints its client
     // ids through the coordinator's `client_seq`, the same counter the
-    // delegated TCP/WS path uses. A separate counter here would let a
+    // delegated TCP/WS/TCP-TLS/WSS paths use. A separate counter here would 
let a
     // shard-0-local id collide with a delegated id that round-robined to
     // shard 0 (both encode target shard 0) in shard 0's connection
     // registry.
@@ -449,30 +444,20 @@ pub(in crate::boot) fn make_shard_zero_client_accept_fns(
 
     let tcp_tls_coord = Rc::clone(&coord);
     let tcp_tls = Rc::new(move |stream, tls_config| {
-        let Some(meta) =
-            client_meta_from_stream(&stream, &tcp_tls_coord, 
ClientTransportKind::TcpTls)
-        else {
-            return;
-        };
-        installer::install_client_tcp_tls(
-            &tcp_tls_bus,
-            meta,
-            stream,
-            tls_config,
-            tcp_tls_request.clone(),
-        );
+        match tcp_tls_coord.delegate_tcp_tls_client(stream, tls_config) {
+            Ok(client_id) => info!(client_id, "TCP-TLS client delegated"),
+            Err(error) => {
+                warn!(error = ?error, "delegate_tcp_tls_client failed; 
dropping TCP-TLS client");
+            }
+        }
     });
 
-    // WSS terminates locally on shard 0 like TCP-TLS (rustls state is not
-    // serialisable across the delegate path), minting ids through the same
-    // coordinator counter.
     let wss_coord = coord;
     let wss = Rc::new(move |stream, tls_config| {
-        let Some(meta) = client_meta_from_stream(&stream, &wss_coord, 
ClientTransportKind::Wss)
-        else {
-            return;
-        };
-        installer::install_client_wss(&wss_bus, meta, stream, tls_config, 
wss_request.clone());
+        match wss_coord.delegate_wss_client(stream, tls_config) {
+            Ok(client_id) => info!(client_id, "WSS client delegated"),
+            Err(error) => warn!(error = ?error, "delegate_wss_client failed; 
dropping WSS client"),
+        }
     });
 
     LocalClientAcceptFns {
@@ -484,21 +469,6 @@ pub(in crate::boot) fn make_shard_zero_client_accept_fns(
     }
 }
 
-fn client_meta_from_stream(
-    stream: &compio::net::TcpStream,
-    coord: &shard::coordinator::ShardZeroCoordinator,
-    transport: ClientTransportKind,
-) -> Option<ClientConnMeta> {
-    let peer_addr = match stream.peer_addr() {
-        Ok(peer_addr) => peer_addr,
-        Err(error) => {
-            warn!(error = %error, "dropping accepted client with unknown peer 
address");
-            return None;
-        }
-    };
-    Some(mint_client_meta(coord, peer_addr, transport))
-}
-
 fn mint_client_meta(
     coord: &shard::coordinator::ShardZeroCoordinator,
     peer_addr: SocketAddr,
diff --git a/core/server/src/boot/mod.rs b/core/server/src/boot/mod.rs
index e7a218695..36850f704 100644
--- a/core/server/src/boot/mod.rs
+++ b/core/server/src/boot/mod.rs
@@ -910,10 +910,9 @@ async fn shard_main(
     }
 
     // Listeners (replica + every client transport) bind on shard 0 only.
-    // Shard 0's coordinator round-robins inbound TCP/WS connections to
-    // peer shards via fd-transfer. QUIC and TCP-TLS clients terminate
-    // locally on shard 0 (their per-connection state is non-portable -
-    // see `LifecycleFrame::ClientWsConnectionSetup` rustdoc).
+    // The coordinator delegates TCP/WS/TCP-TLS/WSS before any handshake;
+    // the destination shard owns all connection state and I/O.
+    // QUIC terminates locally on shard 0 through its shared UDP endpoint.
     if shard_id == 0 {
         let coord = shard
             .coordinator()
diff --git a/core/server/src/dispatch/mod.rs b/core/server/src/dispatch/mod.rs
index 8692bb630..cc2ac9c70 100644
--- a/core/server/src/dispatch/mod.rs
+++ b/core/server/src/dispatch/mod.rs
@@ -130,12 +130,11 @@ where
     })
 }
 
-/// Build the shard's one client-request handler: per-client FIFO queues
-/// drained one task per client, and the bus connection-lost hook that
-/// logs a dropped connection out. Every transport on the shard must
-/// share the instance (shard 0 hands it to its local QUIC, TCP-TLS and
-/// WSS listeners as well), or a client's ordering guarantee and the
-/// disconnect hook split by transport.
+/// Build the shard's client-request handler with per-client FIFO queues
+/// and a connection-lost hook. All connections installed on this shard
+/// share it to preserve ordering and disconnect cleanup across transports.
+/// The destination shard supplies it for delegated TCP/WS/TCP-TLS/WSS
+/// connections; shard 0 also supplies it for local QUIC connections.
 pub fn make_deferred_client_request_handler<B, MJ, S, SB>(
     bus: &B,
     shard_handle: &ShellShardHandle<B, MJ, S, SB>,
diff --git a/core/server/src/dispatch/test_support.rs 
b/core/server/src/dispatch/test_support.rs
index f00b64bca..c5bb3867f 100644
--- a/core/server/src/dispatch/test_support.rs
+++ b/core/server/src/dispatch/test_support.rs
@@ -30,7 +30,7 @@ use message_bus::installer::conn_info::ClientConnMeta;
 use message_bus::replica::listener::MessageHandler;
 use message_bus::{
     BusMessage, ClientConnectionLostFn, ClientForwardFn, ConnectionLostFn, 
JoinHandle, MessageBus,
-    ReplicaForwardFn, ReplicaHandshakeDoneFn, SendError,
+    ReplicaForwardFn, ReplicaHandshakeDoneFn, SendError, SharedTlsServerConfig,
 };
 use metadata::impls::metadata::IggySnapshot;
 use metadata::stm::stream::Streams;
@@ -144,6 +144,22 @@ impl ConnectionInstaller for SpyBus {
         _on_request: RequestHandler,
     ) {
     }
+    fn install_client_tcp_tls_fd(
+        &self,
+        _fd: DupedFd,
+        _meta: ClientConnMeta,
+        _config: SharedTlsServerConfig,
+        _on_request: RequestHandler,
+    ) {
+    }
+    fn install_client_wss_fd(
+        &self,
+        _fd: DupedFd,
+        _meta: ClientConnMeta,
+        _config: SharedTlsServerConfig,
+        _on_request: RequestHandler,
+    ) {
+    }
     fn client_meta(&self, _client_id: u128) -> Option<Rc<ClientConnMeta>> {
         None
     }
diff --git a/core/shard/src/coordinator.rs b/core/shard/src/coordinator.rs
index 72de53324..04825edaf 100644
--- a/core/shard/src/coordinator.rs
+++ b/core/shard/src/coordinator.rs
@@ -25,7 +25,7 @@
 //! 2. duplicates the TCP fd,
 //! 3. sends a connection-setup `LifecycleFrame`
 //!    (`ReplicaInboundSetup` / `ReplicaOutboundSetup` /
-//!    `Client{,Ws}ConnectionSetup`) to the target shard's inbox,
+//!    `Client{,Ws,TcpTls,Wss}ConnectionSetup`) to the target shard's inbox,
 //! 4. drops its own `TcpStream` so only the target shard's wrapped fd
 //!    keeps the socket alive.
 //!
@@ -53,7 +53,7 @@ use crate::metrics::{frame_drop_reason, frame_drop_variant};
 use crate::{LifecycleFrame, ShardCtorError, ShardFrame, TaggedSender, 
validate_sender_ordering};
 use compio::net::TcpStream;
 use message_bus::installer::conn_info::{ClientConnMeta, ClientTransportKind};
-use message_bus::{SendError, fd_transfer};
+use message_bus::{SendError, SharedTlsServerConfig, fd_transfer};
 use std::cell::Cell;
 use std::rc::Rc;
 use tracing::warn;
@@ -222,10 +222,10 @@ impl ShardZeroCoordinator {
     }
 
     /// Mint a client id for a connection that terminates locally on shard 0
-    /// (QUIC, TCP-TLS) instead of being round-robin delegated.
+    /// (QUIC or HTTP) instead of being round-robin delegated.
     ///
     /// Shares the coordinator's `client_seq` counter with the delegated
-    /// TCP/WS path. Both a shard-0-local connection and a delegated
+    /// TCP/WS/TCP-TLS/WSS paths. Both a shard-0-local connection and a 
delegated
     /// connection that round-robined to shard 0 encode `target_shard = 0`
     /// in the top 16 bits; drawing from separate counters would let them
     /// mint the same id and collide in shard 0's single connection
@@ -323,23 +323,9 @@ impl ShardZeroCoordinator {
     /// when the target shard's inbox refuses the setup frame (full or
     /// disconnected).
     pub fn delegate_client(&self, stream: TcpStream) -> Result<u128, 
SendError> {
-        let target = self.next_client_target();
-        let client_id = self.mint_client_id(target);
-        let peer_addr = stream.peer_addr().map_err(SendError::DupFailed)?;
-
-        let fd = fd_transfer::dup_fd(&stream).map_err(SendError::DupFailed)?;
-        let meta = ClientConnMeta::new(client_id, peer_addr, 
ClientTransportKind::Tcp);
-        let setup = LifecycleFrame::ClientConnectionSetup { fd, meta };
-        if let Err(e) = self.senders[target as 
usize].try_send(ShardFrame::lifecycle(setup)) {
-            // The returned frame owns the `DupedFd` and closes it on drop.
-            self.metrics
-                .record_frame_drop(frame_drop_variant::FD_TRANSFER, 
classify_try_send_err(&e));
-            warn!(client_id, target, "delegate_client try_send failed: {e:?}");
-            return Err(SendError::RoutingFailed(target));
-        }
-
-        drop(stream);
-        Ok(client_id)
+        self.ship_client_fd(stream, ClientTransportKind::Tcp, |fd, meta| {
+            LifecycleFrame::ClientConnectionSetup { fd, meta }
+        })
     }
 
     /// Ship a WebSocket client's pre-upgrade TCP connection to the next
@@ -358,19 +344,71 @@ impl ShardZeroCoordinator {
     /// fails or `dup(2)` fails. Returns [`SendError::RoutingFailed`]
     /// when the target shard's inbox refuses the setup frame.
     pub fn delegate_ws_client(&self, stream: TcpStream) -> Result<u128, 
SendError> {
+        self.ship_client_fd(stream, ClientTransportKind::Ws, |fd, meta| {
+            LifecycleFrame::ClientWsConnectionSetup { fd, meta }
+        })
+    }
+
+    /// Delegate a raw TCP socket and its listener's TLS configuration.
+    /// The destination shard owns the TLS handshake and encrypted I/O.
+    ///
+    /// # Errors
+    ///
+    /// Returns the same lookup, duplication and routing errors as
+    /// [`Self::delegate_client`]. Both socket handles close on failure.
+    pub fn delegate_tcp_tls_client(
+        &self,
+        stream: TcpStream,
+        config: SharedTlsServerConfig,
+    ) -> Result<u128, SendError> {
+        self.ship_client_fd(stream, ClientTransportKind::TcpTls, |fd, meta| {
+            LifecycleFrame::ClientTcpTlsConnectionSetup { fd, meta, config }
+        })
+    }
+
+    /// Delegate WSS before TLS or WebSocket state is created. Both
+    /// handshakes and all subsequent I/O run on the destination shard.
+    ///
+    /// # Errors
+    ///
+    /// Returns the same lookup, duplication and routing errors as
+    /// [`Self::delegate_client`]. Both socket handles close on failure.
+    pub fn delegate_wss_client(
+        &self,
+        stream: TcpStream,
+        config: SharedTlsServerConfig,
+    ) -> Result<u128, SendError> {
+        self.ship_client_fd(stream, ClientTransportKind::Wss, |fd, meta| {
+            LifecycleFrame::ClientWssConnectionSetup { fd, meta, config }
+        })
+    }
+
+    #[must_use]
+    pub const fn total_shards(&self) -> u16 {
+        self.total_shards
+    }
+
+    fn ship_client_fd(
+        &self,
+        stream: TcpStream,
+        transport: ClientTransportKind,
+        build_frame: impl FnOnce(fd_transfer::DupedFd, ClientConnMeta) -> 
LifecycleFrame,
+    ) -> Result<u128, SendError> {
         let target = self.next_client_target();
         let client_id = self.mint_client_id(target);
         let peer_addr = stream.peer_addr().map_err(SendError::DupFailed)?;
 
         let fd = fd_transfer::dup_fd(&stream).map_err(SendError::DupFailed)?;
-        let meta = ClientConnMeta::new(client_id, peer_addr, 
ClientTransportKind::Ws);
-        let setup = LifecycleFrame::ClientWsConnectionSetup { fd, meta };
+        let meta = ClientConnMeta::new(client_id, peer_addr, transport);
+        let setup = build_frame(fd, meta);
         if let Err(e) = self.senders[target as 
usize].try_send(ShardFrame::lifecycle(setup)) {
             self.metrics
                 .record_frame_drop(frame_drop_variant::FD_TRANSFER, 
classify_try_send_err(&e));
             warn!(
                 client_id,
-                target, "delegate_ws_client try_send failed: {e:?}"
+                target,
+                ?transport,
+                "client delegation try_send failed: {e:?}"
             );
             return Err(SendError::RoutingFailed(target));
         }
@@ -378,11 +416,6 @@ impl ShardZeroCoordinator {
         drop(stream);
         Ok(client_id)
     }
-
-    #[must_use]
-    pub const fn total_shards(&self) -> u16 {
-        self.total_shards
-    }
 }
 
 /// Map `crossfire::TrySendError` to the `frame_drop_reason` label set so
@@ -412,7 +445,21 @@ fn rr_pick(counter: &Cell<u16>, total_shards: u16, 
skip_zero: bool) -> u16 {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use compio::io::AsyncRead;
     use compio::net::{TcpListener, TcpStream};
+    use message_bus::client_listener::tcp_tls;
+    use message_bus::transports::tls::self_signed_for_loopback;
+    use std::collections::HashSet;
+    use std::sync::Arc;
+    use std::time::Duration;
+
+    const CLIENT_TRANSPORTS: [ClientTransportKind; 4] = [
+        ClientTransportKind::Tcp,
+        ClientTransportKind::Ws,
+        ClientTransportKind::TcpTls,
+        ClientTransportKind::Wss,
+    ];
+    const SOCKET_CLOSE_TIMEOUT: Duration = Duration::from_secs(2);
 
     fn build_senders(total: u16) -> Rc<Vec<TaggedSender>> {
         let mut senders = Vec::with_capacity(total as usize);
@@ -723,7 +770,7 @@ mod tests {
         )
         .expect("coord ctor ok");
 
-        // Interleave shard-0-local mints (QUIC / TCP-TLS path) with
+        // Interleave shard-0-local mints (QUIC / HTTP path) with
         // delegated mints that round-robin to shard 0. Both encode target
         // shard 0 in the top 16 bits; the shared counter keeps every id
         // distinct so they cannot overwrite each other in shard 0's
@@ -898,4 +945,205 @@ mod tests {
             _ => panic!("expected ClientWsConnectionSetup variant"),
         }
     }
+
+    #[compio::test]
+    #[allow(clippy::future_not_send)]
+    async fn mixed_client_transports_share_placement_and_recovered_sequence() {
+        const RECOVERED_SEQUENCE: u128 = 42;
+        const ACCEPTS: u16 = 16;
+        let configs = [test_tls_config(), test_tls_config()];
+        for total_shards in [1, 3, 4] {
+            for skip_zero in [false, true] {
+                let (senders, receivers) = build_senders_with_rx(total_shards);
+                let coord = ShardZeroCoordinator::new(
+                    senders,
+                    total_shards,
+                    CoordinatorConfig {
+                        skip_shard_zero_for_clients: skip_zero,
+                        ..CoordinatorConfig::default()
+                    },
+                    crate::metrics::ShardMetrics::for_shard(),
+                )
+                .unwrap();
+                coord.seed_client_sequence(0, 
std::iter::once(RECOVERED_SEQUENCE));
+                let first_shard = u16::from(skip_zero && total_shards > 1);
+                let mut sequences = HashSet::new();
+
+                for index in 0..ACCEPTS {
+                    let transport = CLIENT_TRANSPORTS[usize::from(index) % 
CLIENT_TRANSPORTS.len()];
+                    let config =
+                        &configs[(usize::from(index) / 
CLIENT_TRANSPORTS.len()) % configs.len()];
+                    let (stream, mut peer) = tcp_pair().await;
+                    let peer_addr = peer.local_addr().unwrap();
+                    let client_id =
+                        delegate_test_client(&coord, stream, transport, 
config).unwrap();
+                    let target = first_shard + index % (total_shards - 
first_shard);
+                    assert_eq!(message_bus::client_id_owning_shard(client_id), 
target);
+                    let sequence = client_id & CLIENT_SEQUENCE_MASK;
+                    assert_eq!(sequence, RECOVERED_SEQUENCE + 
u128::from(index) * 2 + 1);
+                    assert!(sequences.insert(sequence));
+                    
assert!(sequences.insert(coord.mint_shard_zero_client_id()));
+
+                    let frame = 
receivers[usize::from(target)].try_recv().unwrap();
+                    let (fd, meta) = client_setup(frame, transport, config);
+                    assert_eq!(meta.client_id, client_id);
+                    assert_eq!(meta.transport, transport);
+                    assert_eq!(meta.peer_addr, peer_addr);
+                    assert!(
+                        receivers
+                            .iter()
+                            .all(|receiver| receiver.try_recv().is_err())
+                    );
+
+                    drop(fd);
+                    assert_socket_closed(&mut peer).await;
+                }
+            }
+        }
+    }
+
+    #[compio::test]
+    #[allow(clippy::future_not_send)]
+    async fn rejected_client_handoffs_close_both_socket_handles() {
+        let config = test_tls_config();
+        for transport in CLIENT_TRANSPORTS {
+            for disconnected in [false, true] {
+                let (senders, receivers) = build_senders_with_rx(2);
+                if !disconnected {
+                    while senders[1]
+                        .try_send(ShardFrame::lifecycle(
+                            LifecycleFrame::ReplicaInboundHandshakeDone { 
slot: 0 },
+                        ))
+                        .is_ok()
+                    {}
+                }
+                let _receivers = if disconnected {
+                    drop(receivers);
+                    None
+                } else {
+                    Some(receivers)
+                };
+                let coord = ShardZeroCoordinator::new(
+                    senders,
+                    2,
+                    CoordinatorConfig {
+                        skip_shard_zero_for_clients: true,
+                        ..CoordinatorConfig::default()
+                    },
+                    crate::metrics::ShardMetrics::for_shard(),
+                )
+                .unwrap();
+                let (stream, mut peer) = tcp_pair().await;
+                let result = delegate_test_client(&coord, stream, transport, 
&config);
+                assert!(
+                    matches!(result, Err(SendError::RoutingFailed(1))),
+                    "{transport:?}, disconnected={disconnected}: {result:?}"
+                );
+                assert_eq!(Arc::strong_count(&config), 1);
+                assert_socket_closed(&mut peer).await;
+            }
+        }
+    }
+
+    #[compio::test]
+    #[allow(clippy::future_not_send)]
+    async fn 
dropping_queued_tls_setups_releases_sockets_and_listener_configs() {
+        let config = test_tls_config();
+        for transport in [ClientTransportKind::TcpTls, 
ClientTransportKind::Wss] {
+            let (senders, receivers) = build_senders_with_rx(1);
+            let coord = ShardZeroCoordinator::new(
+                senders,
+                1,
+                CoordinatorConfig::default(),
+                crate::metrics::ShardMetrics::for_shard(),
+            )
+            .unwrap();
+            let (stream, mut peer) = tcp_pair().await;
+            delegate_test_client(&coord, stream, transport, &config).unwrap();
+            assert_eq!(Arc::strong_count(&config), 2);
+            drop(coord);
+            drop(receivers);
+            assert_eq!(Arc::strong_count(&config), 1);
+            assert_socket_closed(&mut peer).await;
+        }
+    }
+
+    fn test_tls_config() -> SharedTlsServerConfig {
+        tcp_tls::bind("127.0.0.1:0".parse().unwrap(), 
self_signed_for_loopback())
+            .unwrap()
+            .1
+    }
+
+    #[allow(clippy::future_not_send)]
+    async fn tcp_pair() -> (TcpStream, TcpStream) {
+        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+        let (connected, accepted) = futures::join!(
+            TcpStream::connect(listener.local_addr().unwrap()),
+            listener.accept()
+        );
+        (accepted.unwrap().0, connected.unwrap())
+    }
+
+    fn delegate_test_client(
+        coord: &ShardZeroCoordinator,
+        stream: TcpStream,
+        transport: ClientTransportKind,
+        config: &SharedTlsServerConfig,
+    ) -> Result<u128, SendError> {
+        match transport {
+            ClientTransportKind::Tcp => coord.delegate_client(stream),
+            ClientTransportKind::Ws => coord.delegate_ws_client(stream),
+            ClientTransportKind::TcpTls => {
+                coord.delegate_tcp_tls_client(stream, Arc::clone(config))
+            }
+            ClientTransportKind::Wss => coord.delegate_wss_client(stream, 
Arc::clone(config)),
+            _ => panic!("transport has no TCP delegation path: {transport:?}"),
+        }
+    }
+
+    fn client_setup(
+        frame: ShardFrame,
+        transport: ClientTransportKind,
+        expected_config: &SharedTlsServerConfig,
+    ) -> (fd_transfer::DupedFd, ClientConnMeta) {
+        match (transport, frame) {
+            (
+                ClientTransportKind::Tcp,
+                ShardFrame::Lifecycle(LifecycleFrame::ClientConnectionSetup { 
fd, meta }),
+            )
+            | (
+                ClientTransportKind::Ws,
+                ShardFrame::Lifecycle(LifecycleFrame::ClientWsConnectionSetup 
{ fd, meta }),
+            ) => (fd, meta),
+            (
+                ClientTransportKind::TcpTls,
+                
ShardFrame::Lifecycle(LifecycleFrame::ClientTcpTlsConnectionSetup {
+                    fd,
+                    meta,
+                    config,
+                }),
+            )
+            | (
+                ClientTransportKind::Wss,
+                ShardFrame::Lifecycle(LifecycleFrame::ClientWssConnectionSetup 
{
+                    fd,
+                    meta,
+                    config,
+                }),
+            ) => {
+                assert!(Arc::ptr_eq(&config, expected_config));
+                (fd, meta)
+            }
+            _ => panic!("wrong setup variant for {transport:?}"),
+        }
+    }
+
+    #[allow(clippy::future_not_send)]
+    async fn assert_socket_closed(peer: &mut TcpStream) {
+        let result = compio::time::timeout(SOCKET_CLOSE_TIMEOUT, 
peer.read(vec![0]))
+            .await
+            .expect("delegation must release both socket handles")
+            .0;
+        assert_eq!(result.unwrap(), 0, "peer must observe EOF");
+    }
 }
diff --git a/core/shard/src/lib.rs b/core/shard/src/lib.rs
index 90121ace8..f85c29ecd 100644
--- a/core/shard/src/lib.rs
+++ b/core/shard/src/lib.rs
@@ -58,7 +58,7 @@ use message_bus::client_listener::RequestHandler;
 use message_bus::fd_transfer::DupedFd;
 use message_bus::installer::conn_info::{ClientConnMeta, ClientTransportKind};
 use message_bus::replica::listener::MessageHandler;
-use message_bus::{BusMessage, MessageBus};
+use message_bus::{BusMessage, MessageBus, SharedTlsServerConfig};
 use metadata::IggyMetadata;
 use metadata::impls::metadata::StreamsFrontend;
 use metadata::stm::StateMachine;
@@ -715,7 +715,7 @@ pub enum LifecycleFrame {
     /// Shard 0 distributes an inbound SDK WebSocket client's pre-upgrade
     /// TCP connection fd to the owning shard. The HTTP-Upgrade handshake
     /// has NOT run yet at this point: the fd is plain TCP, the dup is
-    /// safe (cross-shard fd-delegation only happens for plain TCP), and
+    /// safe before any transport state is created, and
     /// `compio_ws::WebSocketStream<TcpStream>`'s `!Send` constraint
     /// (compio `Rc<...>` driver state, post-upgrade) does not apply.
     /// The receiving shard wraps the fd, runs `compio_ws::accept_async`,
@@ -734,6 +734,21 @@ pub enum LifecycleFrame {
         fd: DupedFd,
         meta: ClientConnMeta,
     },
+    /// Delegate TCP-TLS before reading TLS bytes. The destination wraps the
+    /// fd on its runtime and owns the handshake and connection tasks.
+    ClientTcpTlsConnectionSetup {
+        fd: DupedFd,
+        meta: ClientConnMeta,
+        config: SharedTlsServerConfig,
+    },
+    /// Delegate WSS before either handshake. The listener's configuration
+    /// travels with the socket; all TLS and WebSocket state stays local to
+    /// the destination runtime.
+    ClientWssConnectionSetup {
+        fd: DupedFd,
+        meta: ClientConnMeta,
+        config: SharedTlsServerConfig,
+    },
     /// A non-owning shard forwards a replica send to the owning shard's
     /// local bus; the owning shard then takes the fast path.
     ForwardReplicaSend {
@@ -894,6 +909,10 @@ pub enum ShardFrame {
 // rather than as a failing test.
 const _: () = assert!(std::mem::size_of::<ShardFrame>() == 
std::mem::size_of::<LifecycleFrame>());
 
+// Every inbox slot pays for the largest variant, including consensus traffic.
+const MAX_SHARD_FRAME_SIZE: usize = 160;
+const _: () = assert!(std::mem::size_of::<ShardFrame>() <= 
MAX_SHARD_FRAME_SIZE);
+
 impl ShardFrame {
     /// Create a consensus frame addressed to `target_shard`. The sender
     /// is the routing authority; `accept_frame_for_self` compares this
diff --git a/core/shard/src/router.rs b/core/shard/src/router.rs
index dccf089b8..9dec7e986 100644
--- a/core/shard/src/router.rs
+++ b/core/shard/src/router.rs
@@ -747,6 +747,30 @@ where
                 self.bus
                     .install_client_ws_fd(fd, meta, 
self.on_client_request.clone());
             }
+            LifecycleFrame::ClientTcpTlsConnectionSetup { fd, meta, config } 
=> {
+                tracing::info!(
+                    shard = self.id,
+                    client_id = meta.client_id,
+                    raw_fd = fd.as_raw_fd(),
+                    "installing delegated TCP-TLS client fd (pre-handshake)"
+                );
+                self.bus.install_client_tcp_tls_fd(
+                    fd,
+                    meta,
+                    config,
+                    self.on_client_request.clone(),
+                );
+            }
+            LifecycleFrame::ClientWssConnectionSetup { fd, meta, config } => {
+                tracing::info!(
+                    shard = self.id,
+                    client_id = meta.client_id,
+                    raw_fd = fd.as_raw_fd(),
+                    "installing delegated WSS client fd (pre-handshake)"
+                );
+                self.bus
+                    .install_client_wss_fd(fd, meta, config, 
self.on_client_request.clone());
+            }
             LifecycleFrame::ForwardReplicaSend { replica_id, msg } => {
                 if let Err(e) = self.bus.send_to_replica(replica_id, 
msg).await {
                     tracing::debug!(
diff --git a/core/simulator/src/bus.rs b/core/simulator/src/bus.rs
index 7002ea4a1..0434107ed 100644
--- a/core/simulator/src/bus.rs
+++ b/core/simulator/src/bus.rs
@@ -27,7 +27,7 @@ use message_bus::installer::conn_info::ClientConnMeta;
 use message_bus::replica::listener::MessageHandler;
 use message_bus::{
     BusMessage, ClientConnectionLostFn, ConnectionInstaller, MessageBus, 
ReplicaHandshakeDoneFn,
-    SendError,
+    SendError, SharedTlsServerConfig,
 };
 use server_common::{
     MESSAGE_ALIGN, Message,
@@ -368,6 +368,26 @@ impl ConnectionInstaller for SharedSimOutbox {
         panic!("simulator has no fd transfer: ws client install is 
unreachable");
     }
 
+    fn install_client_tcp_tls_fd(
+        &self,
+        _fd: DupedFd,
+        _meta: ClientConnMeta,
+        _config: SharedTlsServerConfig,
+        _on_request: RequestHandler,
+    ) {
+        panic!("simulator has no fd transfer: tcp tls client install is 
unreachable");
+    }
+
+    fn install_client_wss_fd(
+        &self,
+        _fd: DupedFd,
+        _meta: ClientConnMeta,
+        _config: SharedTlsServerConfig,
+        _on_request: RequestHandler,
+    ) {
+        panic!("simulator has no fd transfer: wss client install is 
unreachable");
+    }
+
     // Real, unlike the fd installs above: dispatch reads client_meta to seed
     // the session manager, and registers the disconnect callback the harness
     // fires. Both delegate to the in-memory registry on the inner outbox.

Reply via email to