v2.4.0
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/-6and the ssh_configAddressFamilykeyword now govern direct connections forexec, interactive sessions,ping, and SFTPupload/download, plus port forwarding listeners and targets and every hop of a-Jchain (#246, #248). Precedence follows OpenSSH: command line flag, then config keyword, then theanydefault. - Bracketed IPv6 address literals are accepted in host specifications:
[::1],[::1]:22,user@[::1], anduser@[::1]:22, in-H, SSH-style destinations, and both simple and detailed cluster node entries (#251). bssh pingfollows 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
Hostblock applies toexec,ping,upload, anddownloadrather than only to SSH mode (#249). This coversAddressFamily,Compression,ServerAliveInterval, andServerAliveCountMax. A jump hop now resolves against its ownHost bastionblock 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 genericcould 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 pingalways exited 0 while its help text promised otherwise, so every script branching on it passed unconditionally (#245).-4/-6and ssh_configAddressFamilywere advertised in--helpand the man page, parsed, and then discarded, sobssh -6against 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-tcpiprequest, so a-6invocation could reach IPv4 for every hop after the first (#248). - ssh_config lookups outside SSH mode queried the literal
"*"and missed per-host blocks, sobssh -H v6node uptimeignored aHost v6nodestanza (#249). - Bracketed IPv6 literals died in the hostlist expander before any connection was attempted, because
[and]are its range expression delimiters (#251). A bare::1reached 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
occuredmisspelling and theSshcapitalization in theSshErrorandSftpErrormessages.
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 toNoCheck. Any server key was accepted unconditionally, nothing was written toknown_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 toNoCheckwhen 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. @revokedand@cert-authoritymarker 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@revokedline is a hard rejection, and a@cert-authoritymatch warns. Hostname matching is case-insensitive, and~/.sshandknown_hostsare 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 pingagainst hosts that are all unreachable now exits 255. It previously exited 0, which is also not the 1 the old help text implied.accept-newnow 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-Lor-Dspecification that does not name a bind address listens on::1instead of127.0.0.1, and the*:portwildcard form listens on::instead of0.0.0.0. Scripts that pass-6and 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_nodesreturnsResult<PingOutcome>instead ofResult<()>(#245). Callers that ignored the oldOk(())keep compiling; callers that matched on it exhaustively update the pattern.- The four
SshClient::*_with_jump_hostshelpers take a&SshConnectionConfigand anOption<&SshConnectionConfigResolver>in place of their former trailing argument (#246, #249). ForwardingSpec::parse_local,parse_dynamic, andparsetake anAddressFamilyargument (#246).ForwardingConfiggained a publicaddress_familyfield,bssh::ssh::tokio_client::Errorgained aNoAddressForFamilyvariant, andbssh::ssh::tokio_clientexports the newSshConnectionConfigResolver(#246, #249).
Known Issues
@cert-authoritylines 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-newfalls back to no verification after printing a warning to stderr, since there is no persistent trust state to record against. - The
known_hostsfirst-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
-Land 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