v2.4.1
A patch release that closes the four items listed as known issues in the v2.4.0 release notes.
New Features
None. BSSH_CERT_AUTHORITY_POLICY=reject is new but is an opt-in hardening switch for existing behavior; see Improvements.
Improvements
accept-newkeeps a process-lifetime host key pin when no known_hosts path can be determined, instead of disabling verification entirely (#242). Previously, in containers and service environments whereHOMEis unset and no passwd entry supplies a home directory, the CLI default mode accepted any server key unconditionally. The first key seen for ahost:portis now pinned for the life of the process and a different key later in the same run is rejected. Nothing persists past the process, so this is still weaker than file-backed TOFU.- The known_hosts path is validated before it is treated as trust state (#242). A path that is a non-regular file, cannot be opened, or is a symlink that cannot be resolved now fails the connection and names which of the three it was, in both
accept-newand strict mode. Previously all three were indistinguishable from an absent file, soaccept-newtreated every host as a first use and strict mode treated every host as unknown. A genuinely absent file still behaves as an empty one. - First-use recording is serialized across bssh processes, not just within one (#242). A sibling advisory lock file (
known_hosts.lock, created 0600 under a 0700 parent) now wraps the check-then-record window, and the path probe, marker scan, and lookup all run again under the lock. Two bssh processes connecting to the same new host at once previously could both read known_hosts before either wrote and both append an entry. BSSH_CERT_AUTHORITY_POLICY=rejectmakes a matching known_hosts@cert-authorityline a hard rejection (#242). The default is unchanged: a match warns and falls through to ordinary TOFU, because bssh has no CA signature validation and failing closed would break every working CA setup with no workaround.
Bug Fixes
- Forwarding target hostnames are sent to the server instead of being resolved on the client (#257).
-Ltargets, SOCKS5-Ddomain requests, later jump-chain hops, and the destination behind a jump chain now go out in thedirect-tcpiprequest as written, and the remote sshd resolves them, matching OpenSSH. Any target whose name resolves only from the server's network position previously failed before a single byte of SSH traffic was sent, which is one of the primary reasons to use a bastion. Forcing-4or-6still resolves locally and sends a numeric address, since that is the only way the family request can affect a connection the server makes. - SOCKS4 dynamic forwarding honors the address family preference (#255). The SOCKS4 handler called a family-agnostic opener that hardcoded
AddressFamily::Any, so it ignored-4,-6, and the ssh_configAddressFamilykeyword while every other path in v2.4.0 honored them:bssh -6 -D 1080 user@hosthard-failed every other path when IPv6 was unavailable and still handed back a proxy that tunneled IPv4. A SOCKS4 request under forced IPv6 is now refused with the protocol's 0x5B reply before an SSH channel is opened.Anyand forced IPv4 are unchanged. - SOCKS5 parses IPv6 destination literals (#256). The ATYP 0x04 arm read none of the request body and replied 0x08 "address type not supported", so every SOCKS5 client sending an IPv6 destination was refused and
bssh -6 -D 1080was self-contradictory. The arm now consumes its 16 address bytes and 2 port bytes and forwards a bracketed[ipv6]:porttarget. - A socket address slice reports its first address as the hostname instead of a comma-joined list of every address (#243). A comma is the known_hosts host-list separator, so the joined form is rejected by the hostname validation added in v2.4.0.
- A known_hosts file created by the first recording no longer starts with a blank line (#243). Cosmetic; it was never a parse problem. Files that already existed are untouched.
CI/CD Improvements
None.
Technical Details
- Performance: the known_hosts write locks are skipped when the offered key already matches a recorded entry (#243). The marker scan and lookup run first without either lock and return on a definite match, so parallel connections to an already-recorded cluster no longer serialize on a lock they do not need. Both locks are still taken for a host that looks unknown, with the full check repeated under them so the fast path cannot race a concurrent first-time recording.
- Tests: the nondeterministic
test_expand_path_with_tildefailure under the full library suite is fixed (#243). Seven tests repoint the process-globalHOME, and cargo runs tests in parallel threads, soHOMEcould change betweenexpand_path's internaldirs::home_dir()call and the assertion's own call.EnvGuardnow takes a dedicatedHOMEmutex when it sets or removes that variable, andEnvGuard::lock_home()gives read-only tests the same lock. No library behavior changed. - Validation:
cargo fmt --all --check,cargo clippy --lib --tests --all-targets -- -D warnings,cargo test --lib(1386 passed, 0 failed, 9 ignored), and the focusedaddress_family_testandsocks_ipv6_literal_testsuites all pass on the tagged commit.
Dependencies
No Cargo.toml requirement changed, and no crate entered or left the graph. Seven resolved versions moved in Cargo.lock: aho-corasick 1.1.4 to 1.1.5, darling, darling_core, and darling_macro 0.23.0 to 0.24.0, data-encoding 2.11.0 to 2.11.1, instability 0.3.12 to 0.3.13, and line-clipping 0.3.7 to 0.3.8.
Breaking Changes
None. The library API changed additively only. ServerCheckMethod gained AcceptNewKnownHostsFile(String) and AcceptNewInMemory in both the bssh::ssh::tokio_client enum and its bssh::shared::auth_types mirror, with From conversions in both directions; both enums are #[non_exhaustive], so an exhaustive downstream match already required a wildcard arm. ToSocketAddrsWithHostname gained a host_port method with a default body, so external implementors compile unchanged.
Note one user-visible behavior change that is a fix rather than a break: with no forced address family, forwarding targets are now resolved by the remote server rather than the client (#257). A deployment that relied on client-side resolution of a forwarding target name can force -4 or -6 to restore local resolution.
Known Issues
@cert-authoritylines still fall through to ordinary TOFU by default. bssh has no CA signature validation, so the strict behavior is opt-in throughBSSH_CERT_AUTHORITY_POLICY=rejectrather than the default.- With no determinable known_hosts path,
accept-newpins keys in memory only. The pin does not survive the process, so a first connection in a new process is still an unverified first use. - The cross-process known_hosts lock is advisory and cooperative. It serializes bssh against other bssh processes, not against an unrelated writer editing known_hosts directly.
- SOCKS4 destinations remain IPv4-only by protocol definition. Under forced IPv6 they are now refused rather than silently tunneled, which is the intended behavior but means SOCKS4 and
-6cannot be combined. tests/integration_test.rshas a localhost-auth fixture mismatch: its precheck lets systemsshuse an agent whileParallelExecutor::newdisables agent auth and supplies only~/.ssh/id_rsa, so three localhost cases fail locally in agent-only environments. CI's--skip integration_testdoes not exclude the test binary.
What's Changed
- fix: close host key fail-open paths by @inureyes in #259
- chore: resolve host key verification follow-ups by @inureyes in #260
- fix: preserve forwarded target hostnames by default by @inureyes in #258
- fix: repair merged host-port integration by @inureyes in #262
- fix: honor address family for SOCKS4 forwarding by @inureyes in #263
- fix: support SOCKS5 IPv6 literal destinations by @inureyes in #261
Full Changelog: v2.4.0...v2.4.1