Skip to content

v2.4.1

Choose a tag to compare

@inureyes inureyes released this 03 Aug 13:21
· 32 commits to main since this release

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-new keeps 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 where HOME is unset and no passwd entry supplies a home directory, the CLI default mode accepted any server key unconditionally. The first key seen for a host:port is 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-new and strict mode. Previously all three were indistinguishable from an absent file, so accept-new treated 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=reject makes a matching known_hosts @cert-authority line 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). -L targets, SOCKS5 -D domain requests, later jump-chain hops, and the destination behind a jump chain now go out in the direct-tcpip request 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 -4 or -6 still 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_config AddressFamily keyword while every other path in v2.4.0 honored them: bssh -6 -D 1080 user@host hard-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. Any and 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 1080 was self-contradictory. The arm now consumes its 16 address bytes and 2 port bytes and forwards a bracketed [ipv6]:port target.
  • 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_tilde failure under the full library suite is fixed (#243). Seven tests repoint the process-global HOME, and cargo runs tests in parallel threads, so HOME could change between expand_path's internal dirs::home_dir() call and the assertion's own call. EnvGuard now takes a dedicated HOME mutex when it sets or removes that variable, and EnvGuard::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 focused address_family_test and socks_ipv6_literal_test suites 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-authority lines still fall through to ordinary TOFU by default. bssh has no CA signature validation, so the strict behavior is opt-in through BSSH_CERT_AUTHORITY_POLICY=reject rather than the default.
  • With no determinable known_hosts path, accept-new pins 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 -6 cannot be combined.
  • tests/integration_test.rs has a localhost-auth fixture mismatch: its precheck lets system ssh use an agent while ParallelExecutor::new disables agent auth and supplies only ~/.ssh/id_rsa, so three localhost cases fail locally in agent-only environments. CI's --skip integration_test does 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