You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Forward ChannelMsg::Close to channel before dropping sender (#674)
## Problem
When the remote side sends `CHANNEL_CLOSE`, the client and server
handlers call `self.channels.remove()` which drops the mpsc `Sender`,
causing `Channel::wait()` to return `None`. No `ChannelMsg::Close` is
ever delivered through the channel.
In contrast, the `CHANNEL_EOF` handler correctly sends `ChannelMsg::Eof`
through the channel's mpsc before returning. This inconsistency means
consumers have no way to distinguish a clean remote close from an
unexpected channel teardown.
## RFC context
[RFC 4254 Section
5.3](https://www.rfc-editor.org/rfc/rfc4254#section-5.3) specifies:
> Upon receiving this message, a party MUST send back an
SSH_MSG_CHANNEL_CLOSE unless it has already sent this message for the
channel. The channel is considered closed for a party when it has both
sent and received SSH_MSG_CHANNEL_CLOSE, and the party may then reuse
the channel number.
Importantly, the RFC also states:
> It is not mandatory to send SSH_MSG_CHANNEL_EOF before
SSH_MSG_CHANNEL_CLOSE.
This means `CHANNEL_CLOSE` can arrive as the first and only termination
signal on a channel -- without a preceding `CHANNEL_EOF`. Without this
fix, that close is invisible to consumers: `Channel::wait()` returns
`None` with no way to distinguish "remote cleanly closed" from
"something dropped the channel handle." The `ChannelMsg::Close` variant
exists in the enum but was never delivered through this path.
## Behavioral change
This changes the observable behavior of `Channel::wait()`: consumers now
receive `Some(ChannelMsg::Close)` before `None` when the remote end
sends `CHANNEL_CLOSE`.
Existing `while let Some(msg) = ch.wait().await` loops with `_ => {}`
catch-alls are **unaffected** -- `Close` is silently ignored, the loop
continues, and the next `wait()` returns `None`. However, loops with
catch-all panics that don't handle `Close` will now panic where they
previously exited cleanly via `None`. Two such patterns in the test
suite are hardened in this patch.
## Changes
All changes are in the `russh` crate. No public API signatures are
changed.
### Core fix (client and server)
In both `client/encrypted.rs` and `server/encrypted.rs`, the
`CHANNEL_CLOSE` handler now sends `ChannelMsg::Close` through the
channel's mpsc sender before removing the channel entry from the map.
This matches the existing `CHANNEL_EOF` pattern in both files.
### Documentation
Updated doc comments on `Channel::wait()` and `ChannelReadHalf::wait()`
to document that `ChannelMsg::Close` is delivered before `None` when the
remote end sends `CHANNEL_CLOSE`.
### Tests
- **`test_channel_objects`**: Updated to assert `ChannelMsg::Close` is
received before `None` on the client side.
- **`test_server_receives_close_on_client_close`**: New test validating
the server-side fix -- client opens a channel and closes it, server
observes `ChannelMsg::Close` before `None`.
- **`test_backpressure`**: Added `Some(ChannelMsg::Close) | None =>
break` to the server handler's match arm.
- **`test_data_stream`**: Added `ChannelMsg::Close` alongside
`ChannelMsg::Eof` in the break arm.
---------
Co-authored-by: Eugene <inbox@null.page>
0 commit comments