Skip to content

v2.4.0

Choose a tag to compare

@inureyes inureyes released this 03 Aug 09:48
· 39 commits to main since this release

Host key verification in the default accept-new mode now actually verifies. Address family selection (-4/-6, ssh_config AddressFamily) works across every connection path. bssh ping reports a real exit code.

New Features

  • Address family selection works end to end. -4/-6 and the ssh_config AddressFamily keyword now govern direct connections for exec, interactive sessions, ping, and SFTP upload/download, plus port forwarding listeners and targets and every hop of a -J chain (#246, #248). Precedence follows OpenSSH: command line flag, then config keyword, then the any default.
  • Bracketed IPv6 address literals are accepted in host specifications: [::1], [::1]:22, user@[::1], and user@[::1]:22, in -H, SSH-style destinations, and both simple and detailed cluster node entries (#251).
  • bssh ping follows a 0/1/255 exit code contract, so health checks can branch on it (#245). 0 means every targeted host connected and authenticated, 1 means at least one succeeded and at least one failed, and 255 means no host succeeded or bssh failed before attempting a connection.

Improvements

  • ssh_config connection settings resolve against the actual target host instead of once per dispatch, so a per-host Host block applies to exec, ping, upload, and download rather than only to SSH mode (#249). This covers AddressFamily, Compression, ServerAliveInterval, and ServerAliveCountMax. A jump hop now resolves against its own Host bastion block instead of inheriting the destination's settings.
  • Interactive connection failures print the full anyhow context chain, so a jump-host failure names the hop that failed instead of collapsing to a single outer message (#238).
  • Forcing an address family that has no resolved address fails with no IPv6 address found for <host> (or the IPv4 equivalent) instead of the generic could not resolve to any addresses (#246).
  • The man page describes only current behavior. Two NOTES subsections that were release history moved to or were already covered by the changelog.

Bug Fixes

  • bssh ping always exited 0 while its help text promised otherwise, so every script branching on it passed unconditionally (#245).
  • -4/-6 and ssh_config AddressFamily were advertised in --help and the man page, parsed, and then discarded, so bssh -6 against a dual-stack host could still connect over IPv4 (#246).
  • Jump hops past the first tunneled over the wrong family. Those hops resolve their target locally and send a literal IP in the direct-tcpip request, so a -6 invocation could reach IPv4 for every hop after the first (#248).
  • ssh_config lookups outside SSH mode queried the literal "*" and missed per-host blocks, so bssh -H v6node uptime ignored a Host v6node stanza (#249).
  • Bracketed IPv6 literals died in the hostlist expander before any connection was attempted, because [ and ] are its range expression delimiters (#251). A bare ::1 reached the connect path and failed resolution instead.
  • The intermediate jump-hop error interpolated the jump host twice, printing Failed to connect to jump host bastion (hop 2): bastion.
  • The direct-connect and file-transfer paths built their anyhow chain backwards, so the raw SSH error rendered above the friendly message as a near-duplicate.
  • A context message that restated its own cause printed the same text twice on one line, such as Password authentication failed.: Password authentication failed.
  • Fixed the occured misspelling and the Ssh capitalization in the SshError and SftpError messages.

CI/CD Improvements

None

Technical Details

Four defects in accept-new and strict host key verification are fixed (#239):

  • accept-new, the documented default, mapped to NoCheck. Any server key was accepted unconditionally, nothing was written to known_hosts, and changed keys were never rejected. It now checks the offered key, records unknown hosts with an OpenSSH-style "Permanently added" notice, and rejects changed keys with the standard warning banner including the SHA256 fingerprint, offending file and line, and a remediation hint. Strict (yes) mode no longer downgrades to NoCheck when the known_hosts file is missing.
  • A host with two or more keys of one algorithm was rejected as changed as soon as one entry differed, a false man-in-the-middle alarm whose own remediation would have deleted the legitimate pin. Lookup now applies OpenSSH's rule: any recorded key equal to the offered key verifies.
  • Hostnames were recorded verbatim with no escaping, so a name containing a newline, space, #, or comma could pin a key against an unintended host, make a host permanently unconnectable, or hide the entry from later lookups. Names that cannot round-trip are now rejected before anything is looked up or written.
  • @revoked and @cert-authority marker lines were silently ignored, so an explicitly revoked key was recorded and accepted like any first use. A dedicated scan now runs first: a matching @revoked line is a hard rejection, and a @cert-authority match warns. Hostname matching is case-insensitive, and ~/.ssh and known_hosts are created with mode 0700/0600 directly rather than under the process umask.

SshConnectionConfigResolver is the new seam for per-target settings, combining command line overrides, YAML defaults, and ssh_config Host blocks. It runs at the ParallelExecutor per-node boundary and is threaded through SshClient and JumpHostChain.

The address family filter for tunneled targets reuses open_direct_tcpip_channel_with_family, the same mechanism already used for -L forwarding targets.

Dependencies

No Cargo.toml requirement changed. Locked versions moved: russh 0.62.1 to 0.62.5, tokio 1.52.3 to 1.53.1, clap 4.6.1 to 4.6.5, serde 1.0.228 to 1.0.229, and ratatui 0.30.0 to 0.30.2. The transitive set shifted with them, adding ratatui-termina, termina, palette, sponge-cursor, and approx and dropping scc, sdd, prettyplease, and the wit-bindgen / wasm-encoder / wasmparser toolchain crates.

Breaking Changes

Behavior:

  • bssh ping against hosts that are all unreachable now exits 255. It previously exited 0, which is also not the 1 the old help text implied.
  • accept-new now performs real verification. A changed host key is rejected where it was previously accepted, and connections that silently succeeded against a rotated or substituted key will now fail until the entry is reconciled.
  • With -6, a -L or -D specification that does not name a bind address listens on ::1 instead of 127.0.0.1, and the *:port wildcard form listens on :: instead of 0.0.0.0. Scripts that pass -6 and assume an IPv4 loopback listener should name the bind address explicitly, for example -L 127.0.0.1:8080:example.com:80.
  • An unbracketed IPv6 literal is rejected with IPv6 address literals must be enclosed in brackets, for example '[::1]' instead of failing during resolution.

Library API, for consumers of the bssh crate:

  • bssh::commands::ping::ping_nodes returns Result<PingOutcome> instead of Result<()> (#245). Callers that ignored the old Ok(()) keep compiling; callers that matched on it exhaustively update the pattern.
  • The four SshClient::*_with_jump_hosts helpers take a &SshConnectionConfig and an Option<&SshConnectionConfigResolver> in place of their former trailing argument (#246, #249).
  • ForwardingSpec::parse_local, parse_dynamic, and parse take an AddressFamily argument (#246).
  • ForwardingConfig gained a public address_family field, bssh::ssh::tokio_client::Error gained a NoAddressForFamily variant, and bssh::ssh::tokio_client exports the new SshConnectionConfigResolver (#246, #249).

Known Issues

  • @cert-authority lines warn but do not gate the connection. bssh has no CA signature validation, so a match falls through to ordinary TOFU rather than failing closed, which would break every working CA setup with no workaround.
  • When no home directory can be determined, accept-new falls back to no verification after printing a warning to stderr, since there is no persistent trust state to record against.
  • The known_hosts first-write lock is process-wide. Two concurrent bssh processes recording the same new host are not serialized against each other.
  • SOCKS4 requests carry a literal IPv4 destination by protocol definition and are passed through unfiltered by the address family preference. Forwarding target filtering for -L and SOCKS5 is a best-effort hint, since the remote server performs the actual connect.

What's Changed

  • fix: show full anyhow error chain on interactive connect failure by @inureyes in #240
  • fix(security): implement real TOFU for accept-new host key mode by @inureyes in #241
  • fix: wire up the -4/-6 address family flags and AddressFamily by @inureyes in #247
  • fix: report a real exit code from bssh ping by @inureyes in #250
  • feat: filter tunneled jump targets by address family by @inureyes in #252
  • fix: resolve ssh config per connection target by @inureyes in #254
  • fix: accept bracketed IPv6 host literals (#251) by @inureyes in #253

Full Changelog: v2.3.1...v2.4.0