Skip to content

Commit 56121a7

Browse files
committed
Forward ChannelMsg::Close to channel before dropping sender
When the remote side sends CHANNEL_CLOSE, the client and server handlers were removing the channel entry (dropping the mpsc Sender) without first forwarding ChannelMsg::Close. Consumers on Channel::wait() would see None with no preceding Close message, unlike CHANNEL_EOF which correctly sends ChannelMsg::Eof first. Send ChannelMsg::Close through the channel's mpsc before removing it, matching the existing CHANNEL_EOF pattern.
1 parent 359d708 commit 56121a7

6 files changed

Lines changed: 99 additions & 1 deletion

File tree

russh/src/channels/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ impl std::fmt::Debug for ChannelReadHalf {
153153

154154
impl ChannelReadHalf {
155155
/// Awaits an incoming [`ChannelMsg`], this method returns [`None`] if the channel has been closed.
156+
///
157+
/// When the remote end sends `CHANNEL_CLOSE`, a [`ChannelMsg::Close`] is
158+
/// delivered before this method returns [`None`].
156159
pub async fn wait(&mut self) -> Option<ChannelMsg> {
157160
self.receiver.recv().await
158161
}
@@ -581,6 +584,9 @@ impl<S: From<(ChannelId, ChannelMsg)> + Send + Sync + 'static> Channel<S> {
581584
}
582585

583586
/// Awaits an incoming [`ChannelMsg`], this method returns [`None`] if the channel has been closed.
587+
///
588+
/// When the remote end sends `CHANNEL_CLOSE`, a [`ChannelMsg::Close`] is
589+
/// delivered before this method returns [`None`].
584590
pub async fn wait(&mut self) -> Option<ChannelMsg> {
585591
self.read_half.wait().await
586592
}

russh/src/client/encrypted.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,12 @@ impl Session {
365365
// will not be released.
366366
enc.close(channel_num)?;
367367
}
368+
// Forward the close to the channel before removing it, so that
369+
// consumers waiting on `Channel::wait()` receive an explicit
370+
// `ChannelMsg::Close` instead of just seeing `None`.
371+
if let Some(chan) = self.channels.get(&channel_num) {
372+
let _ = chan.send(ChannelMsg::Close).await;
373+
}
368374
self.channels.remove(&channel_num);
369375
client.channel_close(channel_num, self).await
370376
}

russh/src/server/encrypted.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,12 @@ impl Session {
602602
if let Some(ref mut enc) = self.common.encrypted {
603603
enc.channels.remove(&channel_num);
604604
}
605+
// Forward the close to the channel before removing it, so that
606+
// consumers waiting on `Channel::wait()` receive an explicit
607+
// `ChannelMsg::Close` instead of just seeing `None`.
608+
if let Some(chan) = self.channels.get(&channel_num) {
609+
chan.send(ChannelMsg::Close).await.unwrap_or(())
610+
}
605611
self.channels.remove(&channel_num);
606612
debug!("handler.channel_close {channel_num:?}");
607613
handler.channel_close(channel_num, self).await

russh/src/tests.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,13 @@ mod channels {
455455
panic!("Unexpected message {msg:?}");
456456
}
457457

458+
// After the server closes the channel, we should receive an
459+
// explicit Close message before the channel stream ends.
460+
let msg = ch.wait().await.unwrap();
461+
assert!(
462+
matches!(msg, ChannelMsg::Close),
463+
"expected Close, got {msg:?}"
464+
);
458465
assert!(ch.wait().await.is_none());
459466
c
460467
},
@@ -463,6 +470,78 @@ mod channels {
463470
.await;
464471
}
465472

473+
/// Verify that the server-side CHANNEL_CLOSE handler delivers
474+
/// `ChannelMsg::Close` before the channel stream ends.
475+
#[tokio::test]
476+
async fn test_server_receives_close_on_client_close() {
477+
#[derive(Debug)]
478+
struct Client {}
479+
480+
impl client::Handler for Client {
481+
type Error = crate::Error;
482+
483+
async fn check_server_key(
484+
&mut self,
485+
_server_public_key: &crate::keys::ssh_key::PublicKey,
486+
) -> Result<bool, Self::Error> {
487+
Ok(true)
488+
}
489+
}
490+
491+
struct ServerHandle {
492+
channel: Option<tokio::sync::oneshot::Sender<Channel<server::Msg>>>,
493+
}
494+
495+
impl server::Handler for ServerHandle {
496+
type Error = crate::Error;
497+
498+
async fn auth_publickey(
499+
&mut self,
500+
_: &str,
501+
_: &crate::keys::ssh_key::PublicKey,
502+
) -> Result<server::Auth, Self::Error> {
503+
Ok(server::Auth::Accept)
504+
}
505+
506+
async fn channel_open_session(
507+
&mut self,
508+
channel: Channel<server::Msg>,
509+
_session: &mut server::Session,
510+
) -> Result<bool, Self::Error> {
511+
if let Some(tx) = self.channel.take() {
512+
tx.send(channel).unwrap();
513+
}
514+
Ok(true)
515+
}
516+
}
517+
518+
let (tx, rx) = tokio::sync::oneshot::channel::<Channel<server::Msg>>();
519+
let sh = ServerHandle { channel: Some(tx) };
520+
521+
test_session(
522+
Client {},
523+
sh,
524+
|c| async move {
525+
let ch = c.channel_open_session().await.unwrap();
526+
ch.close().await.unwrap();
527+
c
528+
},
529+
|s| async move {
530+
let mut ch = rx.await.unwrap();
531+
// The server should receive an explicit Close message
532+
// when the client closes the channel.
533+
let msg = ch.wait().await.unwrap();
534+
assert!(
535+
matches!(msg, ChannelMsg::Close),
536+
"expected Close, got {msg:?}"
537+
);
538+
assert!(ch.wait().await.is_none());
539+
s
540+
},
541+
)
542+
.await;
543+
}
544+
466545
#[tokio::test]
467546
async fn test_channel_window_size() {
468547
#[derive(Debug)]

russh/tests/test_backpressure.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ impl russh::server::Handler for Server {
135135
while let Ok(_) = rx.changed().await {
136136
match channel.wait().await {
137137
Some(ChannelMsg::Data { .. }) => (),
138+
Some(ChannelMsg::Close) | None => break,
138139
other => panic!("unexpected message {other:?}"),
139140
}
140141
}

russh/tests/test_data_stream.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ impl ChannelDataCopy for ChannelHalves {
7272
match msg {
7373
ChannelMsg::WindowAdjusted { .. } => {}
7474
ChannelMsg::Data { data } => buf.extend(&*data),
75-
ChannelMsg::Eof => break,
75+
ChannelMsg::Eof | ChannelMsg::Close => break,
7676
msg => panic!("Got unexpected message: {msg:?}"),
7777
}
7878
}

0 commit comments

Comments
 (0)