Skip to content

Commit b044492

Browse files
tentiousclaudesjtrny
authored
fix(server): detect dead clients and reclaim their sockets (#471)
* fix(server): detect dead clients and tear down their data channel pool The server never noticed a disconnected client. The control channel task only wrote heartbeats, and writes keep succeeding on a half-closed socket, so the ControlChannelHandle stayed in the map, its shutdown_tx never dropped, and the data channel pool's listening sockets leaked. Read the control channel concurrently with writes (the client sends nothing after the handshake, so any completed read means it disconnected), and remove the handle from the map when the control channel ends so shutdown_tx drops and the pool is torn down. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(server): harden control channel cleanup Avoid retaining the control-channel map from cleanup tasks and treat unexpected client data as a protocol violation. Cover half-closed channels, map teardown, listener release, and heartbeat-disabled client shutdown. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Stephen Tierney <sjtrny@gmail.com>
1 parent b4173c4 commit b044492

2 files changed

Lines changed: 308 additions & 17 deletions

File tree

src/server.rs

Lines changed: 207 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use backoff::ExponentialBackoff;
1616
use rand::RngCore;
1717
use std::collections::HashMap;
1818
use std::path::Path;
19-
use std::sync::Arc;
19+
use std::sync::{Arc, Weak};
2020
use std::time::Duration;
2121
use tokio::io::{self, copy_bidirectional, AsyncReadExt, AsyncWriteExt};
2222
use tokio::net::{TcpListener, TcpStream, UdpSocket};
@@ -355,8 +355,13 @@ async fn do_control_channel_handshake<T: 'static + Transport>(
355355
conn.flush().await?;
356356

357357
info!(service = %service_config.name, "Control channel established");
358-
let handle =
359-
ControlChannelHandle::new(conn, service_config, server_config.heartbeat_interval);
358+
let handle = ControlChannelHandle::new(
359+
conn,
360+
service_config,
361+
server_config.heartbeat_interval,
362+
Arc::downgrade(&control_channels),
363+
session_key,
364+
);
360365

361366
// Insert the new handle
362367
let _ = h.insert(service_digest, session_key, handle);
@@ -410,6 +415,8 @@ where
410415
conn: T::Stream,
411416
service: ServerServiceConfig,
412417
heartbeat_interval: u64,
418+
control_channels: Weak<RwLock<ControlChannelMap<T>>>,
419+
session_key: Nonce,
413420
) -> ControlChannelHandle<T> {
414421
// Create a shutdown channel
415422
let (shutdown_tx, shutdown_rx) = broadcast::channel::<bool>(1);
@@ -505,12 +512,17 @@ where
505512
heartbeat_interval,
506513
};
507514

508-
// Run the control channel
515+
// On exit, drop the handle so `shutdown_tx` drops and the data channel pool
516+
// closes; otherwise its sockets leak. session_key is unique, so this never
517+
// removes a reconnected session.
509518
tokio::spawn(
510519
async move {
511520
if let Err(err) = ch.run().await {
512521
error!("{:#}", err);
513522
}
523+
if let Some(control_channels) = control_channels.upgrade() {
524+
control_channels.write().await.remove2(&session_key);
525+
}
514526
}
515527
.instrument(Span::current()),
516528
);
@@ -532,25 +544,48 @@ struct ControlChannel<T: Transport> {
532544
}
533545

534546
impl<T: Transport> ControlChannel<T> {
535-
async fn write_and_flush(&mut self, data: &[u8]) -> Result<()> {
536-
write_and_flush(&mut self.conn, data)
537-
.await
538-
.with_context(|| "Failed to write control cmds")?;
539-
Ok(())
540-
}
541547
// Run a control channel
542548
#[instrument(skip_all)]
543-
async fn run(mut self) -> Result<()> {
549+
async fn run(self) -> Result<()> {
544550
let create_ch_cmd = bincode::serialize(&ControlChannelCmd::CreateDataChannel).unwrap();
545551
let heartbeat = bincode::serialize(&ControlChannelCmd::HeartBeat).unwrap();
546552

553+
// Split so we can read (to detect a dead client) and write concurrently.
554+
let ControlChannel {
555+
conn,
556+
mut shutdown_rx,
557+
mut data_ch_req_rx,
558+
heartbeat_interval,
559+
} = self;
560+
let (mut rd, mut wr) = tokio::io::split(conn);
561+
let mut probe = [0u8; 1];
562+
547563
// Wait for data channel requests and the shutdown signal
548564
loop {
549565
tokio::select! {
550-
val = self.data_ch_req_rx.recv() => {
566+
// The client sends nothing after the handshake, so any completed
567+
// read means it's gone. Heartbeat writes alone never notice a
568+
// half-closed client, which leaks its sockets.
569+
res = rd.read(&mut probe) => {
570+
match res {
571+
Ok(0) => {
572+
debug!("Control channel closed by the client");
573+
break;
574+
}
575+
Ok(bytes_read) => {
576+
warn!(bytes_read, "Unexpected data on control channel");
577+
break;
578+
}
579+
Err(e) => {
580+
error!("Control channel read error: {:#}", e);
581+
break;
582+
}
583+
}
584+
},
585+
val = data_ch_req_rx.recv() => {
551586
match val {
552587
Some(_) => {
553-
if let Err(e) = self.write_and_flush(&create_ch_cmd).await {
588+
if let Err(e) = write_and_flush(&mut wr, &create_ch_cmd).await {
554589
error!("{:#}", e);
555590
break;
556591
}
@@ -560,14 +595,14 @@ impl<T: Transport> ControlChannel<T> {
560595
}
561596
}
562597
},
563-
_ = time::sleep(Duration::from_secs(self.heartbeat_interval)), if self.heartbeat_interval != 0 => {
564-
if let Err(e) = self.write_and_flush(&heartbeat).await {
598+
_ = time::sleep(Duration::from_secs(heartbeat_interval)), if heartbeat_interval != 0 => {
599+
if let Err(e) = write_and_flush(&mut wr, &heartbeat).await {
565600
error!("{:#}", e);
566601
break;
567602
}
568603
}
569604
// Wait for the shutdown signal
570-
_ = self.shutdown_rx.recv() => {
605+
_ = shutdown_rx.recv() => {
571606
break;
572607
}
573608
}
@@ -888,3 +923,159 @@ async fn run_udp_connection_pool<T: Transport>(
888923

889924
Ok(())
890925
}
926+
927+
#[cfg(test)]
928+
mod tests {
929+
use super::*;
930+
use std::io::ErrorKind;
931+
use std::net::SocketAddr;
932+
933+
const CLEANUP_TIMEOUT: Duration = Duration::from_secs(5);
934+
935+
fn unused_tcp_addr() -> Result<SocketAddr> {
936+
let listener = std::net::TcpListener::bind("127.0.0.1:0")?;
937+
Ok(listener.local_addr()?)
938+
}
939+
940+
async fn connected_tcp_pair() -> Result<(TcpStream, TcpStream)> {
941+
let listener = TcpListener::bind("127.0.0.1:0").await?;
942+
let addr = listener.local_addr()?;
943+
let (client, (server, _)) = tokio::try_join!(TcpStream::connect(addr), listener.accept())?;
944+
Ok((server, client))
945+
}
946+
947+
async fn wait_until_listener_is_bound(addr: SocketAddr) -> Result<()> {
948+
time::timeout(CLEANUP_TIMEOUT, async {
949+
loop {
950+
match TcpListener::bind(addr).await {
951+
Ok(listener) => {
952+
drop(listener);
953+
time::sleep(Duration::from_millis(10)).await;
954+
}
955+
Err(error) if error.kind() == ErrorKind::AddrInUse => return Ok(()),
956+
Err(error) => return Err(error.into()),
957+
}
958+
}
959+
})
960+
.await
961+
.context("service listener was not created before the deadline")?
962+
}
963+
964+
async fn wait_until_listener_is_released(addr: SocketAddr) -> Result<TcpListener> {
965+
time::timeout(CLEANUP_TIMEOUT, async {
966+
loop {
967+
match TcpListener::bind(addr).await {
968+
Ok(listener) => return Ok(listener),
969+
Err(error) if error.kind() == ErrorKind::AddrInUse => {
970+
time::sleep(Duration::from_millis(10)).await;
971+
}
972+
Err(error) => return Err(error.into()),
973+
}
974+
}
975+
})
976+
.await
977+
.context("service listener was not released before the deadline")?
978+
}
979+
980+
async fn wait_until_channel_is_removed(
981+
control_channels: &RwLock<ControlChannelMap<TcpTransport>>,
982+
service_digest: ServiceDigest,
983+
session_key: Nonce,
984+
) -> Result<()> {
985+
time::timeout(CLEANUP_TIMEOUT, async {
986+
loop {
987+
let removed = {
988+
let channels = control_channels.read().await;
989+
channels.get1(&service_digest).is_none()
990+
&& channels.get2(&session_key).is_none()
991+
};
992+
if removed {
993+
return;
994+
}
995+
time::sleep(Duration::from_millis(10)).await;
996+
}
997+
})
998+
.await
999+
.context("control channel was not removed before the deadline")?;
1000+
Ok(())
1001+
}
1002+
1003+
fn tcp_service(bind_addr: SocketAddr) -> ServerServiceConfig {
1004+
ServerServiceConfig {
1005+
service_type: ServiceType::Tcp,
1006+
name: "cleanup-test".to_owned(),
1007+
bind_addr: bind_addr.to_string(),
1008+
..Default::default()
1009+
}
1010+
}
1011+
1012+
#[tokio::test]
1013+
async fn half_closed_control_channel_releases_map_entry_and_listener_without_heartbeat(
1014+
) -> Result<()> {
1015+
let service_addr = unused_tcp_addr()?;
1016+
let (server_conn, mut client_conn) = connected_tcp_pair().await?;
1017+
let service_digest = [1_u8; HASH_WIDTH_IN_BYTES];
1018+
let session_key = [2_u8; HASH_WIDTH_IN_BYTES];
1019+
let control_channels = Arc::new(RwLock::new(ControlChannelMap::new()));
1020+
1021+
{
1022+
// Match the handshake's locking order so cleanup cannot run before insertion.
1023+
let mut channels = control_channels.write().await;
1024+
let handle = ControlChannelHandle::<TcpTransport>::new(
1025+
server_conn,
1026+
tcp_service(service_addr),
1027+
0,
1028+
Arc::downgrade(&control_channels),
1029+
session_key,
1030+
);
1031+
if channels
1032+
.insert(service_digest, session_key, handle)
1033+
.is_err()
1034+
{
1035+
bail!("failed to insert test control channel");
1036+
}
1037+
}
1038+
1039+
wait_until_listener_is_bound(service_addr).await?;
1040+
client_conn.shutdown().await?;
1041+
1042+
wait_until_channel_is_removed(&control_channels, service_digest, session_key).await?;
1043+
let released_listener = wait_until_listener_is_released(service_addr).await?;
1044+
drop(released_listener);
1045+
drop(client_conn);
1046+
Ok(())
1047+
}
1048+
1049+
#[tokio::test]
1050+
async fn dropping_channel_map_releases_listener_without_retaining_cycle() -> Result<()> {
1051+
let service_addr = unused_tcp_addr()?;
1052+
let (server_conn, _client_conn) = connected_tcp_pair().await?;
1053+
let service_digest = [3_u8; HASH_WIDTH_IN_BYTES];
1054+
let session_key = [4_u8; HASH_WIDTH_IN_BYTES];
1055+
let control_channels = Arc::new(RwLock::new(ControlChannelMap::new()));
1056+
1057+
{
1058+
let mut channels = control_channels.write().await;
1059+
let handle = ControlChannelHandle::<TcpTransport>::new(
1060+
server_conn,
1061+
tcp_service(service_addr),
1062+
0,
1063+
Arc::downgrade(&control_channels),
1064+
session_key,
1065+
);
1066+
if channels
1067+
.insert(service_digest, session_key, handle)
1068+
.is_err()
1069+
{
1070+
bail!("failed to insert test control channel");
1071+
}
1072+
}
1073+
1074+
wait_until_listener_is_bound(service_addr).await?;
1075+
drop(control_channels);
1076+
1077+
let released_listener = wait_until_listener_is_released(service_addr).await?;
1078+
drop(released_listener);
1079+
Ok(())
1080+
}
1081+
}

0 commit comments

Comments
 (0)