Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- **Advertise only `none` SSH compression so clients that negotiate `zlib@openssh.com` no longer drop mid-session** (#215). Cyberduck — and OpenSSH `sftp -C`, or any client that prefers delayed zlib — completed the SFTP handshake (`INIT` / `REALPATH` / `STAT` all succeeded) and then the connection died, with the server logging `SshEncoding: length invalid` on the next inbound packet. Root cause is russh's delayed-zlib (`zlib@openssh.com`) transport: the flate2 stream desyncs a few packets after compression activates post-auth, so russh decodes the following packet's length prefix out of corrupted plaintext. It reproduces on both russh 0.61.1 and 0.62.1, which rules out the channel-close path and pins it to compression rather than the SFTP layer (OpenSSH's default `sftp`, FileZilla, and paramiko all negotiate `none` and were unaffected). `build_russh_config` now sets `russh::Preferred { compression: Cow::Borrowed(&[compression::NONE]), ..DEFAULT }`, so the server advertises only `none` and every client falls back to the uncompressed transport — matching the Dropbear / OpenSSH `sftp-server` defaults used in Backend.AI kernel containers, where compression was never in play. Verified with `sftp -C` (now negotiates `compression: none` and lists/downloads cleanly) and a live Cyberduck 9.5 session. The underlying russh delayed-zlib desync is left to be fixed upstream.

## [2.2.3] - 2026-05-25

### Security
Expand Down
15 changes: 15 additions & 0 deletions src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,23 @@ impl BsshServer {

tracing::info!(key_count = keys.len(), "Loaded host keys");

// russh's delayed zlib (`zlib@openssh.com`) compression desyncs and
// corrupts the channel stream after a few packets, so any client that
// negotiates compression — Cyberduck, `sftp -C` — fails mid-session
// with "SshEncoding: length invalid" (reproducible in russh 0.61.1 and
// 0.62.1). Until the upstream russh bug is fixed, advertise only "none"
// so clients fall back to the uncompressed transport, matching the
// Dropbear/OpenSSH sftp-server defaults used in Backend.AI containers.
// See https://github.com/lablup/bssh/issues/215.
const NO_COMPRESSION: &[russh::compression::Name] = &[russh::compression::NONE];
let preferred = russh::Preferred {
compression: std::borrow::Cow::Borrowed(NO_COMPRESSION),
..russh::Preferred::DEFAULT
};

Ok(russh::server::Config {
keys,
preferred,
auth_rejection_time: Duration::from_secs(3),
auth_rejection_time_initial: Some(Duration::from_secs(0)),
max_auth_attempts: self.config.max_auth_attempts as usize,
Expand Down