# Changelog
All notable changes to bssh will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.3.1] - 2026-07-29
### Added
- **Preserve Kitty keyboard state owned by an outer TUI across interactive PTY sessions** (#236). bssh now queries both terminal screens before reading input, isolates the remote shell from the captured state, forwards unrelated or concurrently typed bytes, and restores the original screen and keyboard modes during every cleanup path.
### Fixed
- **Restore ordinary keyboard input after interactive PTY disconnects** (#234). Cleanup now drains leaked Kitty keyboard stacks on both terminal screens, clears active flags, and disables xterm `modifyOtherKeys`, preventing key presses from appearing as CSI-u fragments such as `;1:3u` or `:1C`.
- **Prevent Homebrew tap updates from corrupting formulae or publishing invalid checksums** (#233). The workflow matches artifact stanzas instead of relying on line positions, rejects failed or malformed downloads, and validates formula syntax, style, version, URLs, and checksums before committing.
### Changed
- **Raise the workspace MSRV and Debian/Launchpad build toolchain from Rust 1.93 to 1.96** (#231, #232). Jammy, Noble, and Resolute now use the `rustc-release` dependency PPA, and `vendor_rust` is pinned to 1.96.0 to match the published toolchain.
### Documentation
- **Remove stale vendored-russh attribution and bound README release history** (#230). NOTICE now lists only source redistributed in-tree, and README points to this changelog instead of maintaining a duplicate full version list.
- **Fix the file-transfer filter example so it compiles as a doctest.** The example now imports `TransferFilter` before calling its `check` method.
## [2.3.0] - 2026-07-18
### Performance
- **Roughly double bssh-server single-connection SFTP write throughput by fixing channel fragmentation and the serial write path** (#187). Three independent bottlenecks identified in the issue's code audit are addressed. First, `build_russh_config` now advertises a 65535-byte `maximum_packet_size` (the russh cap; library default 32768) and an 8 MiB `window_size` (library default 2 MiB), both configurable via `server.maximum_packet_size` / `server.window_size` (env: `BSSH_MAX_PACKET_SIZE` / `BSSH_WINDOW_SIZE`), so a 256 KiB SFTP write is no longer chopped into 8 CHANNEL_DATA packets that each pay the full cipher/copy/scheduling cost. Second, the SFTP server loop in `bssh-russh-sftp` is restructured from a strict read-process-write-flush cycle into a reader task with a bounded read-ahead queue plus an in-order processor: responses are flushed once per burst instead of once per request, and consecutive queued `SSH_FXP_WRITE` requests to the same handle at sequential offsets are coalesced into a single handler call (bounded by `max_write_coalesce_len`, default 256 KiB) while every merged request id still receives its own status reply, preserving SFTP response ordering and error semantics. The bssh write/read handlers additionally track each open file's cursor and elide the per-chunk `seek` for sequential transfers (append handles are exempt since O_APPEND ignores the cursor). Third, the workspace gains a tuned `[profile.release]` (`lto = "fat"`, `codegen-units = 1`) and documented `RUSTFLAGS` target-CPU guidance for self-builds. Local before/after benchmark (loopback, OpenSSH sftp client, 1 GiB random file, 3-run averages, 20-core Linux box): upload 71 MiB/s to 437 MiB/s (6.2x), download 73 MiB/s to 282 MiB/s (3.9x); an ablation with the code changes but library-default channel sizing lands at 333 / 291 MiB/s, attributing most of the gain to the pipelined write path and the rest of the upload gain to the larger packets. Numbers on the reporter's NIC-limited Xeon will differ; the methodology is documented in the PR. Remaining plan items (per-packet copies inside russh, AES-CTR internals) live in upstream russh since #212 removed the fork and are out of scope here.
### Added
- **Make server-side SSH compression configurable instead of hard-disabled** (#220). The #215 workaround forced the server to advertise only `none` compression for every deployment, with no way for operators to opt back in. A new `server.compression` option (YAML `server.compression: true`, env `BSSH_COMPRESSION`, builder `ServerConfigBuilder::compression`) now controls whether `build_russh_config` advertises russh's default compression list (`none`, `zlib`, `zlib@openssh.com`) or only `none`. The default stays off, so behavior is unchanged unless explicitly enabled, and enabling it logs a warning: russh's delayed-zlib (`zlib@openssh.com`) transport still desyncs a few packets after compression activates post-auth (reproduced on russh 0.61.1 and 0.62.1), dropping clients that negotiate it mid-session. Both settings are covered by unit tests, and the option plus the desync caveat are documented in the man page and config docs.
### Fixed
- **Fix an SFTP session deadlock under paramiko's unbounded READ prefetch, and stop stalling sequential SFTP round trips on Nagle's algorithm** (#227). paramiko's `SFTPClient.get()` prefetches by firing READ requests for the whole file with no outstanding-request cap (2048 requests for a 64 MiB file), and any download to such a client froze at exactly the client's initial 2 MiB channel window, with the unread WINDOW_ADJUST messages sitting in the server socket's receive queue. The cycle: the SFTP processor blocks writing a response once the channel window is exhausted; the count-bounded (16) read-ahead queue fills; the reader task stops draining the channel stream; the russh channel rx buffer (100 messages) fills; and the russh session event loop blocks delivering channel data, so it stops reading the socket and never processes the WINDOW_ADJUST that would refill the window. OpenSSH's `sftp` caps outstanding requests at 64 and sshj uses bounded read-ahead, which is why only paramiko (the stack under Ansible and many Python tools) tripped it; the bug predates the #224 pipelining, whose serial predecessor coupled intake to response progress the same way. The SFTP intake queue is now unbounded in request count and bounded in bytes (`max_buffered_request_bytes`, default 8 MiB; a READ request is ~50 bytes, so legitimate pipelining stays far under it): the reader never stops draining while under budget, and a client that floods past it has its session terminated instead of deadlocked. Separately, `build_russh_config` now sets `nodelay: true` on accepted sockets (russh defaults to Nagle on; OpenSSH always sets TCP_NODELAY), removing a ~30 ms delayed-ACK stall per strictly sequential round trip. With both fixes the previously hanging paramiko `get()` completes with byte-for-byte integrity (64 MiB in 0.4 s, 2 GiB verified), and paramiko sequential reads (`prefetch=False`) reach parity with OpenSSH sshd on the same host (15.3 s vs 13.9 s for 64 MiB; the residual per-request latency is client-side, since paramiko itself never sets TCP_NODELAY). Client-initiated disconnects (`Connection reset by peer` and friends) are also no longer logged at ERROR, and regression tests cover the deadlock shape (verified to fail against the previous code), budget-overflow termination, and the disconnect classification.
- **Make `sftp.root`/`scp.root` chroot usable by re-anchoring client paths under the chroot root** (#214). With a chroot configured, directory listing worked but every path resolution failed: `cd subdir`, `get file`, and even `get` of a file sitting directly at the chroot root were rejected as `path outside root` or `not found`, so only bare `/` and `readdir` functioned. Under chroot the client's coordinate space is rooted at `/` (this is what `realpath` reports and what the client sends back), but `resolve_chroot` treated a client absolute path such as `/subdir` as a *host* path and rejected anything not starting with the host root, so `/subdir` (meaning `<root>/subdir`) never matched. A prior change had introduced this to avoid "path doubling", but real clients never send the host path; they send chroot-relative absolute paths, so the guard broke the normal case. The SFTP and SCP resolvers now both interpret absolute and relative client paths relative to `root` (OpenSSH `ChrootDirectory` re-rooting), ignoring a leading `/`, dropping `.`, and clamping `..` so traversal still cannot escape. Containment is verified: `..` stays pinned at the root and a client `/etc/passwd` maps to `<root>/etc/passwd` inside the jail, never the host file. SCP previously kept the old "reject absolute outside root" behavior, so it is unified here to match `sftp.root`. Verified end-to-end with the OpenSSH `sftp` client (`cd`/`get`/root-level files/Unicode names all work; escape attempts stay confined) and covered by updated unit and integration tests.
- **Advertise only `none` SSH compression so clients that negotiate `zlib@openssh.com` no longer drop mid-session** (#215). Cyberduck, OpenSSH `sftp -C`, and 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. The 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.
- **Wire the ssh_config `Compression` directive into the russh client instead of silently ignoring it** (#219). `Compression yes`/`no` was parsed and resolved (`SshHostConfig::compression`) but `to_russh_config` never read it, so it had no effect on the actual connection regardless of the setting. `SshConnectionConfig` gains a `compression` field and `with_compression` builder, and `to_russh_config` now sets `russh::Preferred.compression` from it: `false` (the default, matching `Compression no`/unset) advertises only `none`; `true` (`Compression yes`) advertises eager `zlib` ahead of `none`. `zlib@openssh.com` is deliberately never advertised even when compression is enabled, because the delayed-zlib desync fixed server-side in #215 lives in russh's codec and would equally corrupt a bssh client session. `build_ssh_connection_config` in the dispatcher now resolves `Compression` from ssh_config via the new `SshConfig::get_compression` and feeds it through, so the setting reaches every connection path that shares `SshConnectionConfig` (direct connections, jump-host chains, interactive sessions).
### Security
- **Confine absolute SFTP symlink targets to the chroot** (#214). Once `resolve_chroot` stopped rejecting out-of-root absolute paths, the SFTP `symlink` handler's containment guard became a no-op, so a chrooted client could create a link whose on-disk target was an absolute *host* path (for example `symlink /link /etc/passwd`). Because bssh uses a virtual chroot with no `chroot(2)`, that link resolved to the real host filesystem. Absolute symlink targets are now re-anchored under the chroot before the link is written, so a created link can never point outside the jail; relative targets keep OpenSSH-compatible verbatim storage.
### Changed
- **Raise the workspace minimum supported Rust version from 1.88 to 1.93.** The declared 1.88 floor was already inaccurate: `rustyline` 18 uses `File::lock` (the `file_lock` feature, stabilized in Rust 1.89), so the workspace has not built on 1.88 since `rustyline` was adopted (`cargo +1.88.0 check` fails with E0658; 1.89 and 1.93.0 build clean). CI only built on stable and never caught the drift. `rust-version` is now 1.93, aligned with the Launchpad PPA build toolchain (rustc-1.93 on jammy/noble, in the Ubuntu 26.04 archive on resolute), and a new CI job reads `rust-version` from `Cargo.toml` and runs `cargo check --workspace --locked` on that toolchain so the declared MSRV can no longer silently drift. The Debian packaging `Build-Depends` and the Launchpad PPA vendored toolchain move to rustc-1.93 to match.
- **Unify the auxiliary binaries on the "Broadcast SSH" name** (#210). `bssh-server` and `bssh-keygen` still surfaced "Backend.AI SSH" in their `--help`/version output while the main `bssh` CLI already said "Broadcast SSH"; both are switched to "Broadcast SSH" so all three tools are consistent. `ARCHITECTURE.md` keeps "Broadcast SSH / Backend.AI SSH" to preserve the Backend.AI lineage in the design doc, and the `LICENSE` copyright span is extended to 2024-2026 to match active maintenance.
### Dependencies
- **Drop the vendored `bssh-russh` fork and build against upstream russh 0.62.1** (#212). Both reasons the fork existed are now upstream: the high-frequency PTY output fix (`Handle::data` from spawned tasks, upstream #731 in russh 0.62.0) and the SHA-1 MAC exclusion from `Preferred::DEFAULT` (upstream #690 in russh 0.60.2). The dependency switches from the `path` fork to the crates.io `russh` 0.62.1 and the 66-file vendored crate is removed. `ssh-key` is bumped `=0.7.0-rc.10` to `=0.7.0-rc.11` to match russh 0.62.x, and `channel_open_session` is adapted to the new 0.62 handler signature (a `ChannelOpenHandle` reply plus `Result<()>` instead of `Result<bool>`, accepting the channel via `reply.accept()`). The `.cargo/audit.toml` note for RUSTSEC-2023-0071 is reworded now that no vendored russh fork remains. Verified with a green build, clippy, fmt, and full suite (1233 lib tests, PTY stress 10/10, streaming 28/28).
### Documentation
- **Add an Apache-2.0 `NOTICE` file** (#210) as recommended by Apache License 2.0 section 4(d). It records bssh's own copyright and attributes the vendored upstream forks whose source is redistributed in-tree: russh (originally Thrussh by Pierre-Étienne Meunier, maintained by Eugeny Pankov) and russh-sftp (by AspectUnk), both Apache-2.0. Neither upstream ships a `NOTICE` file, so there is nothing to propagate; the attribution covers the vendored copies and the bssh-specific patches.
- **Document that native Windows client execution is not supported** (#221). A new README "Platform Support" section states that Linux and macOS are fully supported, native Windows client execution is not currently supported, and WSL2 is the recommended path today, linking to #213 for the blocker list; `docs/architecture/ssh-client.md` cross-references it from the existing "SSH agent not supported on Windows" note. No code changes; the native Windows port remains a separately scoped follow-up.
### Tooling
- **Add `tools/bench/`, a self-contained SFTP benchmark, flamegraph, and interop harness** (#228). `bench.sh` measures `bssh-server` SFTP throughput relative to OpenSSH on the same host (with optional before/after comparison via `BSSH_BASELINE_BIN` and single-core pinning to emulate the CPU-bound container scenario from #187); `profile.sh` captures a `perf` plus inferno flamegraph of the upload path behind a `perf_event_paranoid` guard; `interop/` validates round trips and negotiation against sshj and paramiko. Servers bind to loopback only, nothing touches the user's real SSH config, and every transfer is verified byte-for-byte. This harness produced the #187 throughput measurements and surfaced the paramiko read-path deadlock later fixed in #227. No shipped code is touched.
## [2.2.3] - 2026-05-25
### Security
- **Patch RUSTSEC-2026-0009, a stack-exhaustion denial of service in `time`** (#208). `cargo audit` flagged `time` 0.3.45 (medium, 6.8), pulled transitively via `ratatui` 0.30 to `ratatui-widgets` to `time`. Bumped `time` to 0.3.47 (with `num-conv` 0.1.0 to 0.2.2 and `time-core` 0.1.7 to 0.1.8), a lockfile-only change since `ratatui`'s requirement already permits it; `cargo audit` now reports 0 vulnerabilities. Because `time` 0.3.47 requires Rust 1.88, the workspace minimum supported Rust version (`rust-version`) is raised from 1.85 to 1.88; this keeps the MSRV-aware resolver from reverting `time` to the vulnerable 0.3.45 on a future `cargo update`.
### Dependencies
- **Sync both internal russh forks to their latest upstream releases and unify `ssh-key`** (#207). `bssh-russh` advances from a russh 0.60.3 base to **0.61.1**, adopting the new RustCrypto generation upstream migrated to: `sha2` / `sha1` 0.10 to 0.11, `hmac` 0.12 to 0.13, `aes` 0.8 to 0.9, `cbc` 0.1 to 0.2, `ctr` 0.9 to 0.10, `digest` 0.10 to 0.11, `pbkdf2` 0.12 to 0.13, `ssh-key` to 0.7.0-rc.10, and `ssh-encoding` to 0.3.0-rc.9. These cannot be bumped standalone because russh's source targets the old `cipher` 0.4 / `digest` 0.10 API; upstream moved the whole cohort together in 0.61. The high-frequency PTY `Handle::data()` drain fix is re-ported onto the new `server/session.rs` (confirmed still absent upstream in 0.61.1, so the fork remains necessary), three patches now merged upstream are removed (`channel-write-ordering`, `agent-frame-length-cap`, `sha1-mac-exclude`) leaving only `handle-data-fix.patch`, and a PTY regression test is added at `crates/bssh-russh/tests/pty_handle_data.rs`. `bssh-russh-sftp` does a full source sync from upstream 2.1.2 to **2.3.0** with the two pipelined File I/O helpers (`write_all_pipelined` / `read_to_writer_pipelined`) re-applied on top; the bssh SFTP server is adapted to the 2.3.0 `server::Handler::Error` change (now `Into<StatusReply>`, which also surfaces the human-readable error message in `SSH_FXP_STATUS`). In the main crate, `ssh-key` is unified to `=0.7.0-rc.10` so the workspace resolves a single `ssh-key` version instead of 0.6 and 0.7-rc side by side, `argon2` gains its `std` feature (restoring `rand_core`'s `OsRng` after the generation shift), and transitive deps are refreshed via `cargo update`. Both fork crates are published to crates.io at `bssh-russh` 0.61.1 and `bssh-russh-sftp` 2.3.0.
### Changed
- **Fork maintenance tooling and docs refreshed** (#207). Both forks' `create-patch.sh` are now self-contained: they clone the relevant upstream into a temp directory instead of relying on a gitignored `references/` checkout, write the correct current patch names, and fall back to the default branch where upstream publishes no git tags. `sync-upstream.sh` applies every `patches/*.patch` with reverse-apply upstream detection, and both fork READMEs are rewritten for the new versions.
### CI/CD
- Bump `action-gh-release` to v3 for the Node 24 runtime, and adjust the download shield badge.
## [2.2.2] - 2026-05-25
### Fixed
- **Keep idle SSH sessions alive** (#206). Lower the default keepalive interval (`--server-alive-interval`) from 60s to 30s so bssh sends keepalive traffic before common one-minute idle reapers (load balancers, NAT gateways, sshd `ClientAliveInterval`) close otherwise-healthy sessions. Normalize `--server-alive-interval 0` to fully disabled keepalive instead of constructing a zero-duration russh timer, treating `Some(0)` the same as `None` in both the russh client config and the TCP `SO_KEEPALIVE` path. Leave the client-side `inactivity_timeout` disabled unconditionally so a healthy interactive session that legitimately produces no inbound data for a long time (tmux, an idle shell, a long-running REPL) is never torn down locally by bssh; russh's keepalive counter remains the sole liveness detector when keepalive is enabled. Dead-peer detection now resolves in about 120s (three unanswered 30s probes plus the next timer tick that observes them) rather than the previous 180s.
## [2.2.1] - 2026-05-19
### Security
- **Forward-port the SSH-agent half of CVE-2026-46673 (compression ZIP-bomb DoS) into the bssh-russh fork** (#203). Upstream russh v0.60.3 includes the fix as commit `a2d48a7`, but our fork was based on v0.60.1 and did not. Adds `MAX_AGENT_FRAME_LEN = 256 * 1024` and a new `read_frame()` helper in `src/keys/agent/{client,server}.rs` that rejects peer-supplied frame lengths above the cap with `Error::AgentProtocolError` before resizing the receive buffer, blocking the OOM-via-oversized-length-prefix vector. Recorded as `crates/bssh-russh/patches/agent-frame-length-cap.patch` so the next `sync-upstream.sh` run will auto-skip via the reverse-apply dry-run check. The matching `russh-cryptovec` 0.59.0 to 0.60.3 bump in the same PR brings in the cryptovec hardening half of the same CVE (checked-capacity allocation paths, mlock null-pointer guard).
- `cargo audit` against the latest 1090-advisory RustSec database reports **0 vulnerabilities and 0 unmaintained/yanked warnings** after the bumps.
### Dependencies
- **Bump workspace dependencies and sync both russh forks to upstream stable** (#203). Main bssh: `lru` 0.17 to 0.18 (lifetime fix in `get_or_insert_mut_ref`), `signal-hook` 0.3 to 0.4 (only the unused `low_level::pipe` API changed), `opentelemetry` / `opentelemetry_sdk` / `opentelemetry-otlp` 0.31 to 0.32 (`ExportConfig` / `HasExportConfig` removal does not touch our LogExporter / SdkLoggerProvider / Resource / LogRecord call sites), `nix` 0.31.2 to 0.31.3, plus transitive `pin-project`, `tower-http`, `zerofrom` patches. The `bssh-russh` fork advances 0.60.1 to 0.60.3, picking up `aws-lc-rs` 1.16.3 to 1.17.0 and the upstream v0.60.2 unreleased fixes our PR #193 had already forward-ported (#690 SHA-1 MAC exclusion, #693 channel write ordering). The `bssh-russh-sftp` fork does a full source sync from upstream 2.1.1 to 2.1.2: upstream absorbed the original `serde_bytes` perf annotations on `protocol::{Write,Data}.data` plus the matching `serialize_bytes` implementation, so the patch is moved to `patches/historical/` for provenance; the fork's remaining value-add is the two pipelined File I/O helpers (`write_all_pipelined` / `read_to_writer_pipelined`), now re-ported on top of upstream 2.1.2's new `Features` API with chunk sizing derived from `features.limits.{write,read}_len` or `features.max_packet_len.saturating_sub({WRITE,READ}_OVERHEAD_LENGTH)` instead of the removed `MAX_*_LENGTH` constants. `crates/bssh-russh-sftp/Cargo.toml` swaps `flurry` for `dashmap` 6.1.0 and adds `serde_bytes` as a direct dep to match upstream.
### Fixed
- **Add the missing `[dev-dependencies]` block to the bssh-russh fork so its inline test target actually compiles** (#204). The fork's `src/client/test.rs`, `src/keys/mod.rs`, and `src/tests.rs` were imported verbatim from upstream russh during the initial sync (commit `508aa3f0`, "Sync bssh-russh fork with upstream russh 0.60.0"), but the matching `[dev-dependencies]` were never copied across, so `cargo test -p bssh-russh` has failed with E0433 on `env_logger` / `tempfile` and cascading E0282 type-inference errors from day one. PR #203 first surfaced this via `cargo clippy -p bssh-russh --all-targets`. Adds a minimal block covering exactly what the imported tests reference: `env_logger = "0.11"`, `tempfile = "3"`, and `tokio = { version = "1.52.1", features = ["process", "macros"] }` as an additive merge with the main tokio dep (the `process` feature is needed by the spawn-ssh-agent helpers, `macros` by `#[tokio::test]`). 75 tests now run, covering the agent client/server round-trip, PKCS#8 / OpenSSH key decoding, channel lifecycle, GEX, compression, future certificate auth, and server kex junk handling. The agent tests directly exercise the new frame-length cap from PR #203 (the CVE-2026-46673 mitigation) by spawning a real `ssh-agent` over a Unix-domain socket. Workspace aggregate climbs from 1796 to 1871 passed / 0 failed / 10 ignored.
- **Drop a redundant `.into_iter()` in the synced SFTP session loop to satisfy rustc 1.95's stricter `clippy::useless_conversion` lint** (#205). `Iterator::chain` already accepts any `IntoIterator`, so `.chain(files.into_iter())` at `crates/bssh-russh-sftp/src/client/session.rs:194` was redundant. The lint became more aggressive in rustc 1.95.0, breaking CI after the PR #203 sync (the line was imported verbatim from upstream russh-sftp 2.1.2). PR #203 was developed on rustc 1.93.1 where this case did not lint-fire. One-line fix: `.chain(files)` instead of `.chain(files.into_iter())`, functionally identical. Worth filing upstream against `AspectUnk/russh-sftp` so future syncs do not have to re-port this.
## [2.2.0] - 2026-05-18
### Added
- `BSSH_PASSWORD` environment variable for non-interactive password authentication (#201). Documented in the man page and the README environment-variables section. Discouraged for production deployments; recommended for automated test pipelines and CI scenarios where SSH agent or key-based auth is not feasible.
### Fixed
- **`--password` prompt is now collected once up-front and shared across all parallel connection tasks** (#201, closes #200). Previously each per-node SSH task prompted for the password independently via `rpassword::prompt_password()` inside `password_auth()`, racing each other for stdin and interleaving with the indicatif progress UI rendering. The dispatcher now collects the password before any executor or `MultiProgress` is initialized and threads an `Arc<Password>` (backed by `secrecy::SecretString`, auto-zeroized on drop) to every per-node auth task, matching the existing `-S` (`SudoPassword`) pattern. The pre-collected password is also threaded through the jump-host chain (`jump::chain::auth::determine_auth_method`), the SFTP `*_with_jump_hosts` helpers, the download glob-resolution path (`SshClient::connect_and_execute_with_host_check`), and the legacy `execute_command_with_forwarding` path. The in-task `rpassword::prompt_password()` call remains only as the fallback for the OpenSSH-style "all key methods failed" opportunistic prompt path, which has no dispatcher-side collection.
- **`-S` was silently dropped by `ping`, `upload`, and `download`** (#201, closes #200). The dispatcher only read `cli.sudo_password` in the `exec` and interactive branches, so users could pass `-S` to `ping`/`upload`/`download`/`list`/`cache-stats` without any error or feedback. These subcommands now emit a stderr warning explaining that `-S` has no effect, while `exec` and the SSH-mode interactive path continue to honor `-S` as before.
- **`-S` ignored-warning routes to stderr instead of stdout** (#201, follow-up). The previous `tracing::warn!` landed on stdout under the default tracing subscriber, breaking `bssh ... ping | grep ...` pipelines. Switched to `eprintln!` to match the existing pattern used for the `BSSH_PASSWORD` env warning.
### Changed
- Tightened sudo-password collection so it only applies to exec paths that can inject sudo responses, and avoid unused SSH password collection for local-only dispatcher paths.
### Dependencies
- **Drop five stale or redundant direct dependencies** (#199). After auditing the full direct dependency set: `arrayvec` (declared but unused anywhere in the source tree), `ctrlc` (replaced with `tokio::signal::ctrl_c` inside a `tokio::spawn`, guarded by `Handle::try_current` to preserve best-effort semantics outside a runtime), `directories::ProjectDirs`/`BaseDirs` (replaced with `dirs::config_dir`/`dirs::home_dir`, unifying on `dirs` which was already used in 16 other sites and produces equivalent platform paths), `signal-hook` 0.4.4 (downgraded to 0.3 so the project shares the same instance `crossterm` already pulls in via `signal-hook-mio`; usage signatures are identical across both major lines), plus the macOS `objc2`/`block2`/`dispatch2` chain pulled in by `ctrlc`. Three more crates (`lazy_static`, `once_cell`, `fastrand`) remain transitively but are no longer directly referenced after migrating to `std::sync::LazyLock`/`OnceLock` and `rand::random_range`. `Cargo.lock` loses 76 lines including the entire `signal-hook` 0.4.4 subtree.
- **Replace the unmaintained `atty` crate with `std::io::IsTerminal`** (stdlib since Rust 1.70) across PTY, logging, ssh::auth, and the interactive connection (#198). Drops RUSTSEC-2024-0375 (unmaintained) and RUSTSEC-2021-0145 (unsound unaligned read) advisories at once.
- Pick up 33 transitive patch bumps from `cargo update` within current semver constraints (#198): tokio 1.52.1 to 1.52.3, rustls 0.23.39 to 0.23.40, h2 0.4.13 to 0.4.14, digest 0.11.2 to 0.11.3, rpassword 7.4.0 to 7.5.2, and others.
### Documentation
- Document `BSSH_PASSWORD` env var and the new single-prompt password-collection behavior in the README and `bssh.1` man page, including the updated `--password` flag description.
### Security
- Add `.cargo/audit.toml` ignoring RUSTSEC-2023-0071 (RSA Marvin Attack) with an explanatory comment (#198). Both `rsa` 0.9.10 (via `ssh-key` 0.6.x) and `rsa` 0.10.0-rc.17 (via the vendored `bssh-russh` fork) are affected, and no fixed upstream `rsa` release exists. Bumping to 0.10.0-rc.18 conflicts with the `bssh-russh` `pkcs5 = "=0.8.0-rc.13"` pin, so the project accepts the advisory at the audit layer and documents Ed25519/ECDSA as the recommended mitigation for users handling untrusted hosts.
## [2.1.4] - 2026-05-10
### Performance
- **Stream SFTP uploads/downloads instead of buffering whole files in memory** (#195). Previously `upload_file`/`upload_dir_recursive` loaded the entire local file into a `Vec<u8>` via `tokio::fs::read` before calling `write_all`, and `download_file`/`download_dir_recursive` called `read_to_end` into a pooled buffer plus a `clone()` to a separate `Vec` before writing locally. Multi-GB transfers therefore had peak RSS that scaled with file size and large files OOM'd the client. Each path now uses a small `stream_copy()` helper looping on 255 KiB reads/writes through the existing `AsyncRead`/`AsyncWrite` impls on `tokio::fs::File` and `russh_sftp::client::fs::File`. Buffer size matches the SFTP `MAX_WRITE_LENGTH` so each chunk maps to a single SFTP packet without further fragmentation. Verified locally on macOS arm64 against `bssh-server` v2.1.3 over loopback with a 1 GiB file: upload peak RSS drops from ~3.23 GB to ~20 MB and wall time from 38.6 s to 3.5 s; download peak RSS drops from ~2.17 GB to ~16 MB and wall time from 3.93 s to 3.41 s.
- **Pipeline up to 64 concurrent SFTP requests for upload and download** (#196). Bounded pipelined SFTP upload/download helpers replace the previous strictly-sequential request/response loop. Review follow-ups also: cap server-advertised read/write lengths against local maxima to avoid oversized allocations from untrusted SFTP metadata, bound the download reorder queue across both in-flight and pending out-of-order responses, use `fstat` size information where available to avoid reads past EOF and validate unexpected short reads, and preserve remote download handle shutdown after syncing with main. Added SFTP crate tests for chunk-size capping and in-memory pipelined upload/download behavior.
- **Raise bssh-server SFTP `MAX_READ_SIZE` to the 255 KiB SFTP standard** (#197). The server previously hard-capped every SFTP `READ` reply at 64 KiB regardless of what the client requested, while `bssh-russh-sftp` and OpenSSH's `sftp-server` both use the SFTP standard `MAX_READ_LENGTH = 261120` (255 KiB) for request sizing. A client asking for a 256 KiB chunk only ever got 64 KiB back, forcing four extra requests per byte stream. Bumped to 261120 so server replies match the chunk size used by the rest of the stack. Combined with client-side pipelining (#196), this directly cuts the per-MiB request count on downloads from 16 to 4. Memory exposure stays bounded: handles are still capped at `MAX_HANDLES = 1000` per session and each in-flight read still uses a single per-request buffer of this size.
## [2.1.3] - 2026-04-30
### Added
- Internal fork of `russh-sftp` as `crates/bssh-russh-sftp` with a `serde_bytes` performance fix for `SSH_FXP_WRITE` and `SSH_FXP_DATA` packets. The upstream serde derive routes `Vec<u8>` through `deserialize_seq` (byte-by-byte), accounting for ~42% of server CPU during 1 GiB SFTP uploads in `perf` profiling. Annotating the `data` fields with `#[serde(with = "serde_bytes")]` and implementing wire-compatible `serialize_bytes` on the SFTP `Serializer` routes through the existing bulk `deserialize_byte_buf`/`try_get_bytes` path. Measured impact on a CPU-bound host (Xeon Silver 4214): 1 GiB SFTP upload throughput improves from 74.8 MiB/s to 96.4 MiB/s (+29%), closing the gap to OpenSSH `sftp-server` from ~26% to ~5%. (#188)
- `scp.root` configuration field. SCP transfers now honor a chroot setting separate from SFTP. When unset, SCP falls back to `sftp.root`, so a single top-level chroot setting governs both subsystems unless an admin explicitly wants them split. (#186)
### Changed
- Switched the top-level `russh-sftp` dependency from crates.io `russh-sftp = "2.1.1"` to `russh-sftp = { package = "bssh-russh-sftp", version = "2.1.1", path = "crates/bssh-russh-sftp" }`. All existing `use russh_sftp::...` imports continue to work unchanged. (#188)
- **Default file-transfer behavior is no longer chrooted to the user's home directory.** With `sftp.root`/`scp.root` unset (the default), absolute client paths are honored verbatim and relative paths resolve from the user's home directory, matching OpenSSH `sftp-server`/`scp` defaults. Deployments that intentionally want chroot-at-home-dir must now set `sftp.root: <home dir>` (or equivalent) explicitly. (#186)
- Forward-ported unreleased upstream `russh` fixes (#193): exclude SHA-1 MACs from `Preferred::DEFAULT`/`COMPRESSED` (upstream russh #690) and fix channel write ordering when `pending_data` is non-empty (upstream russh #693). Refactored `sync-upstream.sh` to iterate `patches/` and reverse-apply `--dry-run` first so already-merged patches are auto-skipped.
- Bumped dependencies: `tokio` 1.52.1, `clap` 4.6.1, `tracing` 0.1.44, `lru` 0.17, `uuid` 1.23.1, `tokio-util` 0.7.18, `aws-lc-rs` 1.16.3, `ecdsa` rc.17, `elliptic-curve` rc.31, `p256`/`p384`/`p521` rc.9. Pinned `pkcs5="=0.8.0-rc.13"` because `pkcs8` 0.11.0-rc.11 still calls the rc.13-era `Parameters::recommended` API. (#193)
### Fixed
- **bssh-server SCP/SFTP path doubling on absolute client paths** (#186). `ScpHandler::resolve_path` and `SftpHandler::resolve_path_static` previously re-rooted every absolute client path under the user's home directory, so `scp local user@host:/home/work/file.bin` wrote to `/home/work/home/work/file.bin` and `bssh upload local /abs/remote.bin` failed with `No such file`. The resolver now treats absolute client paths verbatim when no chroot is configured and rejects out-of-chroot absolute paths with `permission_denied` when one is. Path-traversal and symlink-escape protections continue to apply.
- **SCP single-file destinations no longer have the source filename appended** (#186). `ScpHandler::receive_file` now consults `target_is_directory` (parsed from `-d`/`-r`) and the filesystem state of the resolved target. `scp local.bin user@host:/tmp/dest.bin` now writes to `/tmp/dest.bin` instead of `/tmp/dest.bin/local.bin`. Directory destinations (`/tmp/dir/`, existing directory, or `-d`/`-r` flag) keep the previous filename-appending behavior.
- **Configured `sftp.root` is no longer dead code** (#186). The handler-construction sites in `SshHandler` previously hard-coded `user_info.home_dir` as the chroot root and ignored `config.sftp.root` entirely. Setting `sftp.root` in the YAML configuration now actually changes the SFTP chroot. The same plumbing now exists for `scp.root`.
- **Chroot bypass via intermediate-directory symlink**. The chroot resolver previously checked only lexical containment for paths whose final component did not exist (typical for new-file creates and `mkdir`). A symlink inside the chroot pointing to a directory outside the chroot would let a client target `chroot/escape/newfile` and have `open(...)`/`create_dir(...)` follow the symlink, writing outside the chroot. Both `ScpHandler::resolve_path` and `SftpHandler::resolve_path_static` now canonicalize the closest existing ancestor of the target path and verify it stays inside the canonicalized chroot, blocking the parent-symlink escape. Found during PR #194 review.
### Documentation
- Standardize man page trailers across `bssh.1`, `bssh-keygen.1`, and `bssh-server.8` into a consistent BUGS / AUTHORS / COPYRIGHT / SEE ALSO order matching common Unix manual conventions. Author attribution, contact email, Apache-2.0 license notice, and project homepage link are now uniform across all three pages. (#192)
- Document `sftp.root` and `scp.root` in `bssh-server.8` configuration sections, and add intermediate-directory-symlink chroot protection to SECURITY CONSIDERATIONS.
### CI/CD
- Bump GitHub Actions to Node.js 24-compatible versions to address Node.js 20 deprecation warnings: `actions/checkout` v4 -> v6, `actions/cache` v4 -> v5, `actions/upload-artifact` v4 -> v7, `apple-actions/import-codesign-certs` v3 -> v7. (#191)
## [2.1.2] - 2026-04-27
### Fixed
- **PTY session mouse tracking leak**: after a PTY session disconnects (normal exit, Ctrl+C, network drop, or panic), the local terminal no longer prints raw SGR mouse escape sequences when the mouse is moved. All cleanup paths (`TerminalStateGuard::Drop`, `force_terminal_cleanup`, and the panic hook via `TerminalGuard`) now emit the full set of mouse-tracking-off sequences (modes 1000, 1002, 1003, 1006, 1015) plus cursor-show and alternate-screen-exit on teardown. (#189, #190)
- **Panic-hook safety in terminal cleanup**: `force_terminal_cleanup()` now uses `try_lock()` instead of `lock()` so the panic hook path (`TerminalGuard::restore_terminal` → `force_terminal_cleanup`) cannot deadlock if the panicking thread already holds `TERMINAL_MUTEX`, and a previously poisoned mutex no longer triggers a secondary panic. The lock only serializes concurrent teardown; the underlying stdout writes and `disable_raw_mode` are individually safe. (#190)
### Changed
- Centralized terminal teardown logic: `TerminalGuard::restore_terminal()` in `interactive_signal.rs` now delegates to `force_terminal_cleanup()` instead of carrying its own incomplete cleanup, closing the gap on the panic-hook path.
### Tests
- Added unit tests for `force_terminal_cleanup()` covering idempotency, poisoned-mutex resilience, and held-mutex resilience (using local `Mutex` instances rather than the global `TERMINAL_MUTEX` to keep the global state undisturbed).
### CI/CD
- Trigger the Homebrew formula update workflow only after the official release: `release.yml` now calls `update_homebrew_formula.yml` via `workflow_call` from the `publish-release` job (which converts pre-release to official), instead of `workflow_run` firing on every Release workflow completion (including pre-release builds).
- Prevent the release workflow from being triggered twice: removed the `published` event type from the trigger list since `publish-release` already handles the pre-release → official conversion. `workflow_dispatch` continues to cover manual runs.
## [2.1.1] - 2026-04-17
### Fixed
- **bssh-server panic on client connection**: `new_client_with_addr` called `block_on()` inside the tokio async runtime to check the auth rate limiter ban list, causing an immediate "Cannot start a runtime from within a runtime" panic on every incoming connection. Added non-blocking `AuthRateLimiter::try_is_banned()` using `RwLock::try_read()`. (#185)
- **bssh-server auth rejection after successful verification**: All `SshHandler` constructors created a local `SessionInfo` that was never registered with `SessionManager`. After public key or password verification succeeded, `authenticate_session()` returned `SessionNotFound`, rejecting the client. Fixed by deferring session creation to the authentication flow via `SessionManager::create_session()`. (#185)
### Changed
- Improved Launchpad PPA packaging for Rust 2024 edition: updated `debian/rules` variants, added `debian/sanitize-vendor.py` for vendored crate checksum sanitization, fixed hidden path handling
- Added `.pyc` files to `.gitignore`
## [2.1.0] - 2026-04-14
### Added
- `EnvGuard` RAII wrapper (`src/test_helpers/env_guard.rs`) for safe environment variable handling in tests, with Drop-based restore semantics and soundness contract documentation (#179, #181)
- Test Environment-Variable Mutation Pattern section in ARCHITECTURE.md documenting the `EnvGuard` soundness contract and `#[serial]` usage
### Changed
- Migrated bssh and the bundled `bssh-russh` crate to the Rust 2024 edition
- Applied 2024 edition clippy improvements: collapsed 38 nested `if let` statements into guard-clause form (`if let Some(x) = y && condition`) across `bssh-russh` client/server/keys/kex modules
- Replaced 177 ad-hoc `unsafe { env::set_var/remove_var }` call sites across 17 test-bearing files with `EnvGuard::set` / `EnvGuard::remove` plus `#[serial]` from the `serial_test` crate
- Removed hand-rolled `ENV_MUTEX` / `once_cell::sync::Lazy<Mutex<()>>` pattern in integration tests in favor of `#[serial]`
- Pinned `bytes` dependency to v1.11.1
### Fixed
- Fixed pattern matching for Rust 2024 edition: removed explicit `ref` / `ref mut` bindings in `if let` patterns in `src/server/handler.rs` and `src/server/shell.rs`
## [2.0.1] - 2026-04-13
### Added
- bssh-keygen now included in Debian package build pipeline for all architectures
### Changed
- Bumped bssh-russh from 0.60.0 to 0.60.1 with security updates
### Fixed
- Fixed GitHub Actions debian_build.yml distro configuration (questing -> resolute)
- Fixed debian_build.yml matrix validation issue causing workflow failures
### Security
- Updated rsa to 0.10.0-rc.17 (fixes RUSTSEC-2023-0071 RSA Marvin Attack)
- Updated RC dependencies to latest versions: elliptic-curve 0.14.0-rc.30, p256/p384/p521 0.14.0-rc.8, ml-kem 0.3.0-rc.2, spki 0.8.0 (stable)
## [2.0.0] - 2026-04-13
### Added
- **bssh-server SSH Server** - A lightweight SSH server designed for container environments
- Full SSH, SFTP, and SCP protocol support
- PTY/shell session support with terminal handling
- Password and public key authentication
- YAML-based comprehensive configuration system
- Command execution handler with security controls
- **Audit Logging Infrastructure**
- File-based audit exporter (JSON Lines format)
- OpenTelemetry audit exporter for observability platforms
- Logstash audit exporter for ELK stack integration
- Configurable audit event types and logging levels
- **Security Features**
- IP-based access control (allow/deny lists)
- Authentication rate limiting (fail2ban-like protection)
- Session management and connection limits
- Path traversal prevention in SFTP handler
- **File Transfer Filtering**
- Path-based filter rules for upload/download control
- Pattern-based filtering with glob support
- Configurable filter actions (allow/deny)
- **bssh-keygen Tool**
- SSH key pair generation utility
- Support for RSA, Ed25519, and ECDSA key types
- Configurable key sizes and comments
- **Server Configuration**
- Per-jump-host SSH private key configuration
- SSH config Host alias reference in jump_host configuration
- SSH keepalive settings in interactive mode
- **Separate Packaging**
- bssh and bssh-server now distributed as separate packages
- Independent Debian packages for client and server
- Separate Homebrew formulas for each component
### Changed
- **Documentation**: Added comprehensive server configuration manual and manpages
- **CI/CD**: Updated release workflow for dual-package distribution; Teams release notification added
- **Dependencies**: Synced bssh-russh fork with upstream `warp-tech/russh` v0.60.0, which brings the RustCrypto chain migration — rand 0.9 → **0.10 stable**, signature 3.0.0-rc.10, ed25519-dalek 3.0.0-pre.6, elliptic-curve 0.14.0-rc.28, p256/p384/p521 0.14.0-rc.7, ecdsa 0.17.0-rc.16, curve25519-dalek 5.0.0-pre.6, der 0.8, sec1 0.8, pkcs8 0.11.0-rc.11, pkcs5 0.8.0-rc.13, spki 0.8.0-rc.4, ml-kem 0.3.0-rc.1, ssh-key 0.6.18, tokio 1.51.1, socket2 0.6.3, signal-hook 0.4.4, fastrand 2.4.1
- **Cluster flag help**: Help examples and man pages now correctly show `-C` (uppercase) for the cluster flag; running the previous `-c` examples failed with clap's "unexpected argument" error
### Fixed
- **SSH idle disconnects**: Interactive sessions could disconnect inconsistently after idle time — sometimes within minutes, sometimes ~10 minutes
- `russh::Config::inactivity_timeout` (10-minute client-side ceiling) is now explicitly set to `None` when keepalive is enabled so the keepalive mechanism alone decides peer liveness
- TCP-level `SO_KEEPALIVE` (via `socket2::TcpKeepalive`) is now applied to every SSH socket so the kernel can detect broken paths even when SSH keepalive replies are dropped by middleboxes
- The exec-mode code path previously dropped the user-configured `SshConnectionConfig` at the `ConnectionConfig` boundary; it now flows through `connect_direct` / `connect_via_jump_hosts` / the jump chain, so `server_alive_interval` actually takes effect in non-interactive runs
- **bssh-server**: Use consistent source package name in Debian `control` file so dual-package builds resolve correctly
- **bssh-server**: Use type inference for `ioctl` to support both glibc and musl builds
### Technical Details
- Shared module structure for client/server code reuse
- russh-based SSH server handler implementation
- Modular audit exporter architecture with trait-based design
- `SshConnectionConfig::to_russh_config()` now overrides `inactivity_timeout` based on keepalive state; `to_tcp_keepalive()` derives a kernel TCP keepalive config from the same settings
- `Client::connect_with_config` rewritten around `russh::client::connect_stream`, building the `TcpStream` manually so SO_KEEPALIVE can be applied before the SSH handshake
- Dropped the `ssh_key::rand_core::OsRng` workaround in key-generation sites; now pass `&mut rand::rng()` (rand 0.10's thread RNG implements the new `rand_core 0.10` `CryptoRng` trait directly)
## [1.7.0] - 2026-01-09
### Added
- **SSH Keepalive Support** (Issue #122)
- `--server-alive-interval` option: Configure keepalive interval in seconds (default: 60, 0 to disable)
- `--server-alive-count-max` option: Maximum keepalive messages without response before disconnect (default: 3)
- Configuration support in config.yaml via `server_alive_interval` and `server_alive_count_max` fields
- Helps maintain long-running sessions through firewalls and NATs that drop idle connections
- Full integration with exec, interactive, and file transfer modes
### Changed
- **Documentation**: Added GitHub downloads badge to README
### Dependencies
- Updated russh from 0.55.0 to 0.56.0
- Updated ratatui from 0.29.0 to 0.30.0
- Updated signal-hook from 0.3.18 to 0.4.1
- Updated whoami from 1.6.1 to 2.0.1
- Updated unicode-width from 0.2.0 to 0.2.2
### Technical Details
- Implemented SSH keepalive packet sending at configurable intervals
- Automatic connection termination after max retries without response
- Keepalive settings work with jump host connections
- Adapted to whoami 2.0 API changes (username() now returns Result)
## [1.6.0] - 2025-12-19
### Added
- **Jump Host Configuration Support in YAML** (Issue #115, PR #120)
- Global defaults level: `defaults.jump_host` for all clusters
- Cluster level: `clusters.<name>.jump_host` for cluster-specific settings
- Node level: Per-node `jump_host` in detailed node configuration
- Environment variable expansion supported (`${VAR}` or `$VAR` syntax)
- Empty string (`""`) explicitly disables jump host inheritance
- CLI `-J` option always takes precedence over configuration
### Changed
- **SSH Config ProxyJump Directive** (Issue #117, PR #119)
- ProxyJump directive from SSH config now properly applied when `-J` option not specified
- Priority order: CLI `-J` > config.yaml jump_host > SSH config ProxyJump
- **Documentation Improvements**
- Added comprehensive jump_host configuration documentation to README.md
- Updated docs/architecture/ssh-jump-hosts.md with detailed architecture
- Updated example-config.yaml with all jump_host configuration patterns
- **CI/CD**
- Updated GitHub workflows
### Fixed
- **Jump Host Authentication** (Issue #116, PR #118)
- Properly handle empty SSH agent when authenticating through jump hosts
- Fall back to key-based authentication when agent has no identities
- **Config Fallback** (PR #120)
- Environment variables now properly expanded in jump_host values via expand_env_vars
- Configuration jump_host properly used in exec and interactive modes
### Technical Details
- Added `ConfigResolver::resolve_jump_host()` method for centralized jump host resolution
- Jump host priority: CLI > Node > Cluster > Global defaults
- Comprehensive test coverage: 424 lines of tests for jump_host configuration
- Integration tests for all priority levels and edge cases
## [1.5.1] - 2025-12-18
### Fixed
- **SSH Disconnect Error Handling** (Issue #113, PR #114)
- Handle SshError(Disconnect) during authentication for password fallback
- Fixed handling of SSH disconnect errors during authentication phase
- Enables proper password fallback when SSH connection is disconnected during auth
## [1.5.0] - 2025-12-18
### Added
- **pdsh Compatibility Mode** (Issues #100-103, #105, #107, #110)
- Full pdsh-style command line compatibility when invoked as `pdsh` or with `--pdsh-compat`
- `-w hosts` option mapped to `-H hosts` for target host specification
- `-x hosts` option mapped to `--exclude hosts` for host exclusion
- `-f N` option mapped to `--parallel N` for fanout control
- `-l user` option for remote username
- `-t N` option mapped to `--connect-timeout N` for connection timeout
- `-u N` option mapped to `--timeout N` for command timeout
- `-N` option mapped to `--no-prefix` for disabling hostname prefix in output
- `-b` option mapped to `--batch` for single Ctrl+C termination
- `-k` option mapped to `--fail-fast` for stop on first failure
- `-q` query mode to show target hosts and exit
- `-S` option mapped to `--any-failure` for returning largest exit code
- **Hostlist Expressions** (Issue #107)
- pdsh-style range expansion: `node[1-5]` → node1, node2, node3, node4, node5
- Zero-padded ranges: `node[01-05]` → node01, node02, node03, node04, node05
- Comma-separated values: `node[1,3,5]` → node1, node3, node5
- Cartesian product: `rack[1-2]-node[1-3]` → 6 hosts
- Domain suffix support: `web[1-3].example.com`
- User and port preservation: `admin@db[01-03]:5432`
- File input with `^/path/to/hostfile`
- **In-TUI Log Panel** (Issue #106)
- Toggle visibility with `l` key
- Color-coded by level: ERROR (red), WARN (yellow), INFO (white), DEBUG (gray)
- Configurable buffer size via `BSSH_TUI_LOG_MAX_ENTRIES` (default: 1000, max: 10000)
- Panel height adjustable from 3-10 lines with `+`/`-` keys
- Scroll with `j`/`k` keys, toggle timestamps with `t`
- **--fail-fast Option** (Issue #103)
- `-k` / `--fail-fast` flag to stop immediately on first failure
- Compatible with pdsh `-k` option
- Cancels pending commands when any node fails
- Can be combined with `--require-all-success` for strict error handling
- **--batch Option** (Issue #102)
- `-b` / `--batch` flag for single Ctrl+C termination
- Compatible with pdsh `-b` option
- Useful for non-interactive scripts and CI/CD pipelines
- **--exclude Option** (Issue #100)
- `--exclude` / `-x` for host exclusion
- Supports wildcards, glob patterns, and hostlist expressions
- Applied after `--filter` option
- **--no-prefix Option** (Issue #101)
- `-N` / `--no-prefix` for disabling hostname prefix in output
- Compatible with pdsh `-N` option
- Works with both stream mode and file mode
- **--connect-timeout Option** (PR #103)
- Separate connection timeout from command execution timeout
- Default: 30 seconds, minimum: 1 second
- Useful for fast failure detection on unreachable hosts
### Changed
- **CI Workflow Simplification**
- Merged multiple jobs into single pipeline for efficiency
### Fixed
- **--timeout 0 Handling** (Issue #112)
- Fixed --timeout 0 to correctly treat as unlimited execution time
- Previously the 0 value was being ignored, causing unexpected timeout behavior
- Added explicit CLI test to prevent regression
- **Environment Variable Test Race Conditions**
- Added `#[serial]` attribute to env var tests to prevent race conditions
- Tests now run sequentially when accessing shared environment state
- **Connect Timeout Propagation**
- Fixed connect_timeout not being propagated through all SSH connection paths
### Documentation
- **Architecture Restructure** (Issue #109)
- Restructured ARCHITECTURE.md into modular documentation
- Removed residual dates and fixed incomplete sentences
- **pdsh Compatibility Documentation** (Issue #110)
- Added comprehensive pdsh migration guide (docs/pdsh-migration.md)
- Added pdsh options reference (docs/pdsh-options.md)
- Added pdsh usage examples (docs/pdsh-examples.md)
- Added installation scripts for pdsh symlink setup
## [1.4.2] - 2025-12-16
### Fixed
- **PTY Session Terminal Handling** (PR #90)
- Fixed terminal escape sequence responses displayed on first prompt when starting tmux
- Improved terminal compatibility with multiplexers
- **PTY Session Paste** (PR #89)
- Fixed paste not working in PTY sessions
- Improved clipboard/paste functionality in interactive terminal sessions
### Changed
- **Dependencies**
- Bumped dependencies to latest versions for security and compatibility
## [1.4.1] - 2025-12-16
### Added
- **Comprehensive Test Suite** (Issue #82, PR #86)
- tests/tui_snapshot_tests.rs: 20 tests for TUI rendering using ratatui's TestBackend
- tests/tui_event_tests.rs: 36 tests for keyboard navigation, scroll behavior, and view transitions
- tests/streaming_integration_tests.rs: 28 tests for NodeStream, MultiNodeStreamManager, and streaming execution
- benches/large_output_benchmark.rs: Performance benchmarks for large output handling
- Total: 84 new tests added
### Fixed
- **SSH Agent Password Fallback** (Issue #84, PR #85)
- Extended password fallback to handle all SSH agent authentication failures
- Now correctly triggers for: AgentAuthenticationFailed, AgentNoIdentities, AgentConnectionFailed, AgentRequestIdentitiesFailed
- Added `is_auth_error_for_password_fallback()` helper function for testability
- Added unit tests and integration tests for all error types
### Documentation
- **TUI Module Documentation** (Issue #81, PR #83)
- Added comprehensive TUI architecture section to ARCHITECTURE.md
- Enhanced README.md with keyboard shortcuts reference table
- Added view modes description table
- Added TUI activation conditions and requirements
- Added missing "1-9: Jump to node N" shortcut in detail view help text
### Dependencies
- Added insta 1.34 (snapshot testing)
- Added criterion 0.5 (benchmarking)
- Added mockall 0.12 (mocking for integration tests)
### Technical Details
- Refactored test code quality: removed unused functions, replaced panic! with matches! macro
- Added Unicode test cases (Korean, Chinese, Emoji)
- Improved assertion messages for better debugging
## [1.4.0] - 2025-12-15
### Added
- **Sudo Password Support** (Issue #74, PR #78)
- `-S/--sudo-password` flag for automated sudo authentication
- Securely prompts for sudo password before command execution
- Automatically detects and responds to sudo password prompts
- Works with both streaming and non-streaming execution modes
- `BSSH_SUDO_PASSWORD` environment variable support (with security warnings)
- Uses `secrecy` crate for secure memory handling
- Password cleared from memory immediately after use
- **Developer Tooling**
- Added githooks for development workflow
- Setup script for githooks configuration
### Fixed
- **Password Fallback** (PR #80)
- Improved SSH debugging for better compatibility
- Enhanced password authentication fallback logic
- Fixed clippy warnings for useless_vec and same_item_push
## [1.3.0] - 2025-12-10
### Added
- **Interactive TUI (Terminal User Interface)** (Phase 3 of #68)
- Summary view: All nodes at a glance with progress bars
- Detail view (1-9): Full output from specific node with scrolling
- Split view (s): Monitor 2-4 nodes simultaneously
- Diff view (d): Compare output from two nodes side-by-side
- Auto-scroll (f): Toggle automatic scrolling
- Navigation: Arrow keys, PgUp/PgDn, Home/End
- Help (?): Show keyboard shortcuts
- Press `q` to quit
- **Multi-node Stream Management** (Phase 2 of #68)
- Real-time output modes for multi-node operations
- Stream mode with [node] prefixes for real-time monitoring
- Enhanced streaming infrastructure for real-time updates
### Fixed
- **PTY Escape Sequence Filtering** (PR #77)
- Filter terminal escape sequence responses in PTY sessions
- Fixed issue with terminal response codes appearing in output
### Technical Details
- Implemented ratatui-based TUI rendering
- Added multi-node output aggregation and display
- Performance optimizations for real-time output streaming
## [1.2.2] - 2025-10-29
### Fixed
- **Backend.AI Auto-detection** (PR #66)
- Improved host heuristics for Backend.AI environments
- Added localhost and localhost.localdomain detection
- Added IPv4 address validation (127.0.0.1, 192.168.x.x, etc.)
- Enhanced detection for user@host, host:port, FQDN, IPv6 patterns
- Users can now use `bssh localhost "command"` naturally in Backend.AI
### Technical Details
- Extracted testable `looks_like_host_specification()` function
- Added `is_ipv4_address()` helper with strict validation
- Performance optimized with early returns
- 16 comprehensive tests added for host detection logic
## [1.2.1] - 2025-10-28
### Fixed
- **Password Authentication Fallback in Interactive Mode** (PR #65)
- Re-implemented password authentication fallback logic for interactive mode
- Fixed issue where password prompt was not appearing after key-based authentication failed
- Ensured proper authentication flow in interactive sessions
- **Test Race Condition** (commit d2e8ce9)
- Added `#[serial]` attribute to tests calling `RankDetector` to prevent environment variable race conditions
- Tests now run sequentially when accessing shared environment state
- Prevents intermittent test failures due to concurrent environment variable access
## [1.2.0] - 2025-10-27
### BREAKING CHANGES
- **Exit code behavior changed** (Issue #62)
- **Old behavior (v1.0-v1.1)**: Returns 0 only if all nodes succeeded, 1 if any failed
- **New behavior (v1.2.0+)**: Returns main rank's actual exit code (matches MPI standard: mpirun, srun, mpiexec)
- **Impact**: Users relying on "all nodes must succeed" semantics must add `--require-all-success` flag
- **Benefit**: Preserves actual exit codes (139=SIGSEGV, 137=OOM, 124=timeout) for better diagnostics
- **MPI users**: No changes needed - behavior improved and matches standard tools
- **Health checks**: Add `--require-all-success` flag to preserve old behavior
### Added
- **Exit Code Strategy** (Issue #62)
- Main rank exit code returned by default (matches MPI standard: mpirun, srun, mpiexec)
- Preserves actual exit codes: 139 (SIGSEGV), 137 (OOM), 124 (timeout), etc.
- `--require-all-success` flag for v1.0-v1.1 behavior (returns 0 only if all nodes succeed)
- `--check-all-nodes` flag for hybrid mode (main rank code + all-node check)
- Automatic main rank detection via `BACKENDAI_CLUSTER_ROLE` environment variable
- Example scripts: `examples/mpi_exit_code.sh`, `examples/health_check.sh`
### Changed
- Exit code behavior now aligns with HPC and distributed computing best practices
- Enables sophisticated error handling in shell scripts and CI/CD pipelines
## [1.1.0] - 2025-10-24
### Added
- **macOS Keychain Integration** (Issue #59, PR #61)
- Automatic passphrase storage in macOS Keychain after successful SSH key authentication
- Automatic passphrase retrieval before prompting user
- Secure memory handling with `Zeroizing` for all sensitive data
- Integration with SSH config `UseKeychain` option per host
- New module `src/ssh/keychain_macos.rs` with complete Keychain API wrapper
- **ProxyUseFdpass SSH Option** (Issue #58, PR #60)
- Added `ProxyUseFdpass` SSH configuration option support
- Optimizes ProxyCommand usage by passing connected file descriptors back to ssh
- Reduces overhead from lingering processes and extra read/write operations
- **Password Authentication Fallback**
- Automatic password retry when publickey authentication fails
- Matches OpenSSH standard behavior for seamless user experience
- Interactive terminal detection with TTY checks
- Works for both exec and interactive modes
### Security
- **SSH Key File Ownership Validation** (PR #61)
- Prevents storing passphrases for SSH keys owned by other users
- Added macOS user ID checks using libc
- World-readable SSH key permission warnings
- **User Consent for Password Fallback** (PR #61)
- Explicit user consent prompt before attempting password authentication
- 30-second timeout for consent prompt
- Prevents unexpected password prompts that could lead to credential exposure
- **Rate Limiting** (PR #61)
- 100ms delay before initial connection attempts
- 1 second delay before password fallback
- Prevents brute-force attacks and fail2ban triggers
### Improved
- **Code Quality** (PR #61)
- Eliminated 251 lines of code duplication in connection logic
- Created `establish_connection()` helper function
- Centralized authentication logic in auth module
- 60% reduction in connection.rs complexity
- **Cross-Platform Support**
- All macOS-specific code properly isolated with `#[cfg(target_os = "macos")]`
- Conditional imports to prevent unused code warnings on non-macOS platforms
- Stub functions for API consistency across platforms
### Fixed
- Fixed clippy warnings on non-macOS platforms (unused_mut, unused_imports, dead_code)
- Variable shadowing for macOS-specific code paths
- Conditional imports for platform-specific functions
- Fixed interactive mode missing `use_keychain` field causing authentication failures
- Fixed password prompt not appearing when connecting to new servers in interactive mode
### Dependencies
- Added `security-framework = "2.12.1"` for macOS Keychain API integration
- Added `libc` for macOS user ID checks (conditional on macOS)
## [1.0.0] - 2025-10-24
### Added
- **SSH Configuration: Certificate Authentication Options**
- `CertificateFile` - Specify SSH certificate files for PKI authentication (maximum 100 certificates)
- `CASignatureAlgorithms` - Define CA signature algorithms for certificate validation (maximum 50 algorithms)
- `HostbasedAuthentication` - Enable/disable host-based authentication
- `HostbasedAcceptedAlgorithms` - Specify accepted algorithms for host-based authentication (maximum 50 algorithms)
- **SSH Configuration: Advanced Port Forwarding Control**
- `GatewayPorts` - Control remote port forwarding access (yes/no/clientspecified)
- `ExitOnForwardFailure` - Terminate connection when port forwarding fails
- `PermitRemoteOpen` - Specify allowed destinations for remote TCP port forwarding (maximum 1000 entries)
- **SSH Configuration: Command Execution and Automation Options**
- `PermitLocalCommand` - Allow execution of local commands after successful SSH connection (yes/no, default: no)
- `LocalCommand` - Execute local command after connection with token substitution support (%h, %H, %n, %p, %r, %u, %%)
- `RemoteCommand` - Execute command on remote host instead of starting interactive shell
- `KnownHostsCommand` - Execute command to obtain host keys dynamically (supports token substitution)
- `ForkAfterAuthentication` - Fork SSH process to background after successful authentication (yes/no)
- `SessionType` - Specify session type: none (port forwarding only), subsystem (e.g., SFTP), or default (shell)
- `StdinNull` - Redirect stdin from /dev/null for background operations and scripting (yes/no)
- **SSH Configuration: Host Key Verification & Security Options**
- `NoHostAuthenticationForLocalhost` - Skip host key verification for localhost connections (convenient for local development, default: no)
- `HashKnownHosts` - Hash hostnames in known_hosts file to prevent hostname disclosure if compromised (default: no)
- `CheckHostIP` - Check host IP address in known_hosts for DNS spoofing detection (deprecated in OpenSSH 8.5+, retained for legacy compatibility)
- `VisualHostKey` - Display ASCII art of host key fingerprint for visual verification (default: no)
- `HostKeyAlias` - Specify alias for host key lookup in known_hosts (useful for load-balanced services with shared keys)
- `VerifyHostKeyDNS` - Verify host keys using DNS SSHFP records (yes/no/ask, default: no)
- `UpdateHostKeys` - Accept updated host keys from server automatically (yes/no/ask, default: no)
- **SSH Configuration: Additional Authentication Options**
- `NumberOfPasswordPrompts` - Control password authentication retry attempts (valid range: 1-10, default: 3)
- `EnableSSHKeysign` - Enable ssh-keysign for host-based authentication (yes/no, default: no)
- **SSH Configuration: Network & Connection Options**
- `BindInterface` - Bind SSH connection to specific network interface (alternative to BindAddress for multi-homed hosts)
- `IPQoS` - Set IP type-of-service/DSCP values for interactive and bulk traffic (e.g., "lowdelay throughput")
- `RekeyLimit` - Control SSH session key renegotiation frequency (format: "data [time]", e.g., "1G 1h")
- **SSH Configuration: X11 Forwarding Options**
- `ForwardX11Timeout` - Set timeout for untrusted X11 forwarding connections (time interval, default: 0 = no timeout)
- `ForwardX11Trusted` - Enable trusted X11 forwarding with full display access (yes/no, default: no)
- **Security Enhancements**
- Path validation to prevent usage of sensitive system files (e.g., /etc/passwd, /etc/shadow)
- Memory exhaustion prevention with entry limits for certificates and forwarding rules
- Algorithm list validation with maximum entry limits
- Deduplication for certificate files and remote forwarding destinations
- Command injection prevention for LocalCommand and KnownHostsCommand
- Token validation to prevent invalid substitution patterns
- Dangerous character detection in command strings (semicolons, backticks, pipes, etc.)
### Changed
- **SSH Config Parser**: Refactored into modular structure for better maintainability
- Split oversized parser.rs (1706 lines) into category-based modules (~200-350 lines each)
- Organized option parsing by categories: authentication, security, forwarding, connection, etc.
- Improved code organization and maintainability
### Technical Details
- Enhanced SSH configuration merging logic with proper priority handling
- Support for both "Option Value" and "Option=Value" syntax
- Scalar options override in later blocks, vector options accumulate with deduplication
- **SSH Configuration Coverage**: ~71 options (~69% of OpenSSH's 103 options)
- Basic options + Include + Match directives (structural)
- Certificate authentication and port forwarding (7 options)
- Command execution and automation (7 options)
- Host key verification, authentication, network, and X11 options (15 options)
- Comprehensive test coverage: 278 tests including parser, resolver, integration, and security tests
- Validation: NumberOfPasswordPrompts range checking (1-10), CheckHostIP deprecation warnings
## [0.9.1] - 2025-10-14
### Added
None
### Changed
- **PTY Terminal Modes**: Complete implementation of PTY terminal modes for better interactive session support
- **Shift Key Input Support**: Full Shift key input handling in PTY mode for proper terminal behavior
### Fixed
- Terminal mode implementation for PTY sessions
- Shift key input handling in interactive mode
### Technical Details
- Enhanced terminal mode settings for PTY allocation
- Implemented proper terminal flag handling for interactive sessions
- Improved keyboard input processing for special keys
## [0.9.0] - 2025-10-14
### Added
- **Configurable Jump Host Limit**: Maximum number of jump hosts can now be configured via environment variable
- `BSSH_MAX_JUMP_HOSTS` environment variable for dynamic limit configuration
- Default: 10 jump hosts, Absolute maximum: 30 (security cap)
- Invalid/zero values fall back to default with warning logs
- Example: `BSSH_MAX_JUMP_HOSTS=20 bssh -J host1,...,host20 target`
- Prevents resource exhaustion attacks while allowing flexible configurations
- **Jump Host File Transfer Support**: Added complete file transfer operations through SSH jump hosts
- `upload_file_with_jump_hosts()` - Upload single files through jump host chains
- `download_file_with_jump_hosts()` - Download single files through jump hosts
- `upload_dir_with_jump_hosts()` - Upload directories recursively through jump hosts
- `download_dir_with_jump_hosts()` - Download directories through jump hosts
- All file transfer operations now fully support multi-hop SSH connections
- **Jump Host Interactive Mode Support**: Interactive shell sessions now work through jump hosts
- Added `jump_hosts` field to `InteractiveCommand` structure
- Dynamic timeout calculation based on hop count (30s base + 15s per hop)
- Prevents premature timeouts on multi-hop connections
- Full authentication support (SSH keys, agent, password) for each hop
- **Parallel Executor Integration**: Jump host support across all parallel operations
- Updated `executor.rs` to propagate jump_hosts to all node operations
- Maintains backward compatibility with `Option<&str>` type
- All `*_to_node()` functions now accept `jump_hosts` parameter
### Changed
- **Interactive Mode**: Now includes jump host support with automatic timeout adjustment
- Connection timeout scales with hop count for reliability
- Example timeouts: Direct (30s), 1 hop (45s), 2 hops (60s), 3 hops (75s)
- **Test Coverage**: Updated all test files to include jump_hosts parameter
- `tests/interactive_test.rs`: Added `jump_hosts: None` to test cases
- `tests/interactive_integration_test.rs`: Updated all 9 test instances
- `examples/interactive_demo.rs`: Updated example with jump_hosts field
- **Dependencies**: Updated various dependencies for security patches and stability
### Fixed
- Interactive mode timeout issues when connecting through jump hosts
- File transfer operations not working with jump host chains
### Security
- Added `serial_test` dependency for thread-safe environment variable testing
- Comprehensive test coverage for environment variable functionality (6 new tests)
### Technical Details
- **Files Modified**: 8 files
- **Lines Added**: +623
- **Lines Removed**: -26
- **Net Change**: +597 lines
- **Test Results**: All 132 tests passing
## [0.8.0] - 2025-09-12
### Added
- Comprehensive SSH port forwarding support
- Local port forwarding (`-L`) for tunneling to remote services
- Remote port forwarding (`-R`) for exposing local services
- Dynamic port forwarding (`-D`) for SOCKS4/5 proxy functionality
- Improved error messages with better context and recovery suggestions
### Changed
- Removed dangerous `unwrap()` calls throughout codebase
- Enhanced error handling with detailed failure reasons
## [0.7.0] - 2025-08-30
### Added
- SSH jump host infrastructure (`-J` option)
- OpenSSH ProxyJump format parsing
- Multiple jump hosts support (comma-separated)
- IPv6 address handling with bracket notation
- Jump host chain management and connection establishment
### Changed
- Improved Ubuntu PPA support with better packaging
- Fixed deprecated GitHub Actions workflows
## [0.6.1] - 2025-08-28
### Changed
- Rebranded from "Backend.AI SSH" to "Broadcast SSH"
- Emphasizes core broadcast/parallel functionality
- Better reflects the tool's primary purpose
## [0.6.0] - 2025-08-28
### Added
- SSH configuration file support (`-F` option)
- Auto-loads from `~/.ssh/config` by default
- Supports 40+ SSH directives
- Wildcard pattern matching and negation
- Environment variable expansion in paths
- PTY allocation for interactive sessions (`-t`/`-T` options)
- SSH configuration caching for improved performance
- LRU cache with configurable size and TTL
- File modification detection
- 10-100x faster repeated operations
### Changed
- Enhanced security with improved host key verification
- Performance improvements across core operations
- SSH-compatible command-line interface (drop-in replacement)
## [0.5.4] - 2025-08-27
### Fixed
- Parallel configuration value handling issues
- Interactive mode authentication alignment with exec mode
## [0.5.3] - 2025-08-27
### Changed
- Backend.AI cluster auto-detection now uses cluster SSH key configuration
## [0.5.2] - 2025-08-27
### Fixed
- Configuration file loading priority issues
- Backend.AI environment variable handling improvements
### Changed
- Now uses cluster SSH key configuration when available
## [0.5.1] - 2025-08-25
### Added
- Configurable command timeout support
- Set timeout via `--timeout` flag or configuration file
- Support for unlimited execution time (`timeout=0`)
- Default timeout: 300 seconds (5 minutes)
## [0.5.0] - 2025-08-22
### Added
- Interactive mode with PTY support
- Single-node mode for focused interaction
- Multiplex mode for parallel command execution
- Node switching commands (`!node1`, `!node2`, etc.)
- Broadcast command (`!broadcast <cmd>`)
- Visual status indicators (● active, ○ inactive)
- Command history with rustyline
- Configurable prompts and settings
### Changed
- Improved Backend.AI cluster auto-detection
- Enhanced interactive shell capabilities
## [0.4.0] - 2025-08-22
### Added
- Password authentication support (`-P` flag)
- SSH key passphrase support with secure prompting
- Modern UI with semantic colors and Unicode symbols
- Debian package distribution (`.deb`)
### Changed
- XDG Base Directory specification compliance
- Improved configuration management
- Enhanced visual feedback and progress indicators
## [0.3.0] - 2025-08-22
### Added
- Native SFTP directory operations
- Recursive file transfer support
- Upload directories with `-r` flag
- Download entire directory trees
- Glob pattern support for batch operations
## [0.2.0] - 2025-08-21
### Added
- Backend.AI multi-node session support
- Automatic cluster detection from environment variables
- Default SSH port 2200 for Backend.AI clusters
- SSH authentication enhancements
- SSH agent authentication with auto-detection
- Host key verification with known_hosts support
- Multiple authentication method fallback
- Environment variable expansion in configuration files
- Connection and command timeout configuration
- SFTP file transfer (upload/download)
- SCP-compatible file copy functionality
- Progress tracking for file operations
### Changed
- Improved error messages and diagnostics
- Enhanced security with host key verification
## [0.1.0] - 2025-08-21
### Added
- Initial release of bssh
- Parallel SSH command execution across multiple nodes
- Cluster configuration management via YAML files
- Node specification via CLI (`-H` flag)
- SSH key-based authentication
- Real-time progress tracking with progress bars
- Per-node output collection and aggregation
- Configurable parallel execution limits
- Connectivity testing (`ping` command)
- Cluster listing (`list` command)
### Features
- Built with Rust for performance and safety
- Async/await pattern for maximum concurrency
- Tokio runtime for efficient I/O operations
- russh library for native SSH implementation
- Cross-platform support (Linux and macOS)
[2.3.1]: https://github.com/lablup/bssh/compare/v2.3.0...v2.3.1
[2.3.0]: https://github.com/lablup/bssh/compare/v2.2.3...v2.3.0
[2.2.3]: https://github.com/lablup/bssh/compare/v2.2.2...v2.2.3
[2.2.2]: https://github.com/lablup/bssh/compare/v2.2.1...v2.2.2
[2.2.1]: https://github.com/lablup/bssh/compare/v2.2.0...v2.2.1
[2.2.0]: https://github.com/lablup/bssh/compare/v2.1.4...v2.2.0
[2.1.4]: https://github.com/lablup/bssh/compare/v2.1.3...v2.1.4
[2.1.3]: https://github.com/lablup/bssh/compare/v2.1.2...v2.1.3
[2.1.2]: https://github.com/lablup/bssh/compare/v2.1.1...v2.1.2
[2.1.1]: https://github.com/lablup/bssh/compare/v2.1.0...v2.1.1
[2.1.0]: https://github.com/lablup/bssh/compare/v2.0.1...v2.1.0
[2.0.1]: https://github.com/lablup/bssh/compare/v2.0.0...v2.0.1
[2.0.0]: https://github.com/lablup/bssh/compare/v1.7.0...v2.0.0
[1.7.0]: https://github.com/lablup/bssh/compare/v1.6.0...v1.7.0
[1.6.0]: https://github.com/lablup/bssh/compare/v1.5.1...v1.6.0
[1.5.1]: https://github.com/lablup/bssh/compare/v1.5.0...v1.5.1
[1.5.0]: https://github.com/lablup/bssh/compare/v1.4.2...v1.5.0
[1.4.2]: https://github.com/lablup/bssh/compare/v1.4.1...v1.4.2
[1.4.1]: https://github.com/lablup/bssh/compare/v1.4.0...v1.4.1
[1.4.0]: https://github.com/lablup/bssh/compare/v1.3.0...v1.4.0
[1.3.0]: https://github.com/lablup/bssh/compare/v1.2.2...v1.3.0
[1.2.2]: https://github.com/lablup/bssh/compare/v1.2.1...v1.2.2
[1.2.1]: https://github.com/lablup/bssh/compare/v1.2.0...v1.2.1
[1.2.0]: https://github.com/lablup/bssh/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/lablup/bssh/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/lablup/bssh/compare/v0.9.1...v1.0.0
[0.9.1]: https://github.com/lablup/bssh/compare/v0.9.0...v0.9.1
[0.9.0]: https://github.com/lablup/bssh/compare/v0.8.0...v0.9.0
[0.8.0]: https://github.com/lablup/bssh/compare/v0.7.0...v0.8.0
[0.7.0]: https://github.com/lablup/bssh/compare/v0.6.1...v0.7.0
[0.6.1]: https://github.com/lablup/bssh/compare/v0.6.0...v0.6.1
[0.6.0]: https://github.com/lablup/bssh/compare/v0.5.4...v0.6.0
[0.5.4]: https://github.com/lablup/bssh/compare/v0.5.3...v0.5.4
[0.5.3]: https://github.com/lablup/bssh/compare/v0.5.2...v0.5.3
[0.5.2]: https://github.com/lablup/bssh/compare/v0.5.1...v0.5.2
[0.5.1]: https://github.com/lablup/bssh/compare/v0.5.0...v0.5.1
[0.5.0]: https://github.com/lablup/bssh/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/lablup/bssh/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/lablup/bssh/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/lablup/bssh/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/lablup/bssh/releases/tag/v0.1.0