You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
bssh declares OpenSSH-compatible -4/--ipv4 and -6/--ipv6 flags, but no code path ever reads them. They parse successfully, appear in --help, are documented in the man page, and silently do nothing.
A user who runs bssh -6 -H host "uptime" against a dual-stack host may still get an IPv4 connection. The flags are advertised behavior that does not exist.
Declaration site, src/cli/bssh.rs lines 280 to 296:
#[arg( short = '4', long = "ipv4", conflicts_with = "ipv6", help = "Force use of IPv4 addresses only")]pub ipv4: bool,#[arg( short = '6', long = "ipv6", conflicts_with = "ipv4", help = "Force use of IPv6 addresses only")]pub ipv6: bool,
conflicts_with is wired correctly, so mutual exclusion already works. Only the consumption is missing.
Proof they are unused.rg -n 'ipv4|ipv6' src/ --type rust outside src/cli/bssh.rs returns only unrelated hits:
src/hostlist/parser.rs (is_ipv6_start, bracket parsing in hostlist expressions)
src/utils/sanitize.rs (hostname validation)
src/app/initialization.rs (is_ipv4_address, used for SSH single-host mode detection)
src/cli/pdsh.rs lines 321 and 322, which only hardcode ipv4: false, ipv6: false when building a Cli from pdsh-compat arguments
None of them reads cli.ipv4 or cli.ipv6.
The AddressFamily config keyword is dead in the same way.AddressFamilyis parsed (src/ssh/ssh_config/parser/options/connection.rs line 102), stored (src/ssh/ssh_config/types.rs line 78, pub address_family: Option<String>), and merged during host-config resolution (src/ssh/ssh_config/resolver.rs lines 214 to 215). It is never read anywhere else, so the value round-trips through the config layer and is then discarded. Since the plumbing already exists, honoring AddressFamily belongs in this issue rather than a follow-up, and the OpenSSH precedence rule applies: the command line flag overrides the config keyword.
Expected behavior
Match OpenSSH semantics:
-4 forces IPv4 only. Only IPv4 socket addresses are candidates.
-6 forces IPv6 only. Only IPv6 socket addresses are candidates.
Neither flag means try all resolved addresses in resolver order. This is today's behavior and must remain the default.
AddressFamily any|inet|inet6 in SSH config applies the same constraint at lower precedence than the command line flag.
When the forced family yields no candidate address, fail with a specific error naming the family and the host, not the current generic message.
Affected code paths
1. Primary connect path (in scope)
src/ssh/tokio_client/connection.rs, connect_with_config_inner (starts at line 337). It resolves the target and iterates every candidate address with no family filter:
let socket_addrs = addr
.to_socket_addrs().map_err(super::Error::AddressInvalid)?;letmut connect_res:Result<...> = Err(super::Error::AddressInvalid(io::Error::new(
io::ErrorKind::InvalidInput,"could not resolve to any addresses",)));for socket_addr in socket_addrs {
...
let stream = match tokio::net::TcpStream::connect(socket_addr).await{ ...};
The fix is to filter socket_addrs by SocketAddr::is_ipv4() / SocketAddr::is_ipv6() when a family is forced, and to produce a clear error when the filter leaves no candidates (for example no IPv6 address found for <host> instead of the current could not resolve to any addresses).
The addr parameter is an impl ToSocketAddrsWithHostname, so the family preference cannot be encoded in the address type. It has to be threaded in as a separate argument or carried on the existing Config struct.
2. Jump hosts (first hop in scope, later hops out of scope)
The first hop goes through JumpChain::connect_to_first_jump (src/jump/chain.rs line 350), which reaches the same connect_with_config_inner path, so it is covered by fix 1.
Later hops in src/jump/chain/tunnel.rs call russh::client::connect_stream (lines 122 and 234) on a stream that already exists (a channel through the previous hop). No local TCP connect happens there. The remote sshd performs the resolution, which bssh cannot influence.
One nuance worth deciding on: src/jump/chain/tunnel.rs lines 97 and 220 do call to_socket_addrs().next(), but only to build a SocketAddr for ClientHandler (host key verification context and display), not to open a socket. Taking .next() unconditionally means the family recorded there is also not controllable, so a mismatch between the recorded address and the actual connection could confuse known_hosts diagnostics.
3. Port forwarding listeners (decision required)
src/forwarding/local.rs line 250: TcpListener::bind(self.bind_addr)
src/forwarding/dynamic/forwarder.rs line 210: TcpListener::bind(self.bind_addr)
bind_addr currently defaults to IpAddr::V4(Ipv4Addr::LOCALHOST) (src/forwarding/spec.rs line 55 for Local, line 105 for Remote).
In OpenSSH, AddressFamily also constrains which addresses forwarded ports listen on. This issue must decide whether -6 makes -L/-D bind to ::1 instead of 127.0.0.1.
Related: ChannelManager::open_direct_tcpip_channel (src/ssh/tokio_client/channel_manager.rs line 169) resolves the forwarding target locally and iterates candidates to send in the direct-tcpip request. Whether the forced family should filter that list is a separate question, since the address is interpreted by the remote sshd rather than connected to locally.
4. bssh-server (out of scope)
src/server/mod.rs line 277 (TcpListener::bind(addr)) belongs to the bssh-server binary, which has its own CLI in src/bin/bssh_server.rs. Out of scope unless the server CLI grows the same flags.
Decision points
Answer these before implementation:
Forwarding listeners. Does -6 change the default listen address for -L/-D from 127.0.0.1 to ::1? OpenSSH's AddressFamily does constrain forwarding listeners, but changing this default is user-visible and could break scripts that assume IPv4 loopback. If yes, does an explicit bind_address in the forwarding spec override the flag?
Forwarding targets. Should the forced family filter the candidate list passed to open_direct_tcpip_channel, given that the remote sshd performs the actual connect?
Jump host hops beyond the first. Confirm they are out of scope, and document that limitation in the man page so it is explicit rather than surprising.
Handler address in tunnel.rs. Should the SocketAddr used for ClientHandler respect the forced family, for consistency with host key records?
Failure mode. What exact error text and exit code when the forced family yields no address? Proposal: a distinct error variant carrying the hostname and requested family, surfaced as no IPv6 address found for <host>, using the same non-zero exit path as other connection failures. Confirm the wording, and confirm it is a hard failure with no fallback to the other family (which is what OpenSSH does).
AddressFamily values. Accept any, inet, inet6 case-insensitively. Decide the behavior for an unrecognized value: hard parse error, or warn and treat as any.
Acceptance criteria
Integration into the real code flow is required. A standalone helper function that nothing calls does not satisfy this issue.
-4 and -6 are read from Cli and threaded through to connect_with_config_inner, so the flag demonstrably changes which address is connected to. Verified on a dual-stack host, or with a test double that asserts the family of the connected SocketAddr.
AddressFamily any|inet|inet6 from SSH config is honored, with the command line flag taking precedence over the config keyword.
Forcing a family that has no resolved address fails with a specific error naming the host and the requested family, not the generic could not resolve to any addresses.
No behavior change when neither the flag nor AddressFamily is set: all resolved addresses are tried in resolver order.
First jump hop honors the flag (it shares the primary connect path). Behavior for later hops is documented.
The decisions above about forwarding listeners and forwarding targets are implemented as decided, or explicitly documented as intentionally unaffected.
Unit test asserting the address filter: given a mixed IPv4/IPv6 candidate list, -4 yields only IPv4 candidates, -6 yields only IPv6, and neither flag yields the original list unchanged and in order.
Test covering the empty-after-filter error path.
Test covering command line over config precedence.
Docs updated. docs/man/bssh.1 already documents -4 at line 82 and -6 at line 86 with the text "Force use of IPv4/IPv6 addresses only", so the man page currently describes behavior that does not exist. Update those entries with the actual scope once implemented (which paths are affected, the jump host limitation, forwarding behavior), and document AddressFamily support.
Notes
Found while refreshing #75. A proposed network-ping command would be the natural first consumer of the address family preference, since connectivity testing is exactly where forcing a family matters most. Whoever picks up #75 should depend on the preference being plumbed through rather than reimplementing the filter locally.
Problem
bsshdeclares OpenSSH-compatible-4/--ipv4and-6/--ipv6flags, but no code path ever reads them. They parse successfully, appear in--help, are documented in the man page, and silently do nothing.A user who runs
bssh -6 -H host "uptime"against a dual-stack host may still get an IPv4 connection. The flags are advertised behavior that does not exist.Declaration site,
src/cli/bssh.rslines 280 to 296:conflicts_withis wired correctly, so mutual exclusion already works. Only the consumption is missing.Proof they are unused.
rg -n 'ipv4|ipv6' src/ --type rustoutsidesrc/cli/bssh.rsreturns only unrelated hits:src/hostlist/parser.rs(is_ipv6_start, bracket parsing in hostlist expressions)src/utils/sanitize.rs(hostname validation)src/app/initialization.rs(is_ipv4_address, used for SSH single-host mode detection)src/cli/pdsh.rslines 321 and 322, which only hardcodeipv4: false, ipv6: falsewhen building aClifrom pdsh-compat argumentsNone of them reads
cli.ipv4orcli.ipv6.The
AddressFamilyconfig keyword is dead in the same way.AddressFamilyis parsed (src/ssh/ssh_config/parser/options/connection.rsline 102), stored (src/ssh/ssh_config/types.rsline 78,pub address_family: Option<String>), and merged during host-config resolution (src/ssh/ssh_config/resolver.rslines 214 to 215). It is never read anywhere else, so the value round-trips through the config layer and is then discarded. Since the plumbing already exists, honoringAddressFamilybelongs in this issue rather than a follow-up, and the OpenSSH precedence rule applies: the command line flag overrides the config keyword.Expected behavior
Match OpenSSH semantics:
-4forces IPv4 only. Only IPv4 socket addresses are candidates.-6forces IPv6 only. Only IPv6 socket addresses are candidates.AddressFamily any|inet|inet6in SSH config applies the same constraint at lower precedence than the command line flag.Affected code paths
1. Primary connect path (in scope)
src/ssh/tokio_client/connection.rs,connect_with_config_inner(starts at line 337). It resolves the target and iterates every candidate address with no family filter:The fix is to filter
socket_addrsbySocketAddr::is_ipv4()/SocketAddr::is_ipv6()when a family is forced, and to produce a clear error when the filter leaves no candidates (for exampleno IPv6 address found for <host>instead of the currentcould not resolve to any addresses).The
addrparameter is animpl ToSocketAddrsWithHostname, so the family preference cannot be encoded in the address type. It has to be threaded in as a separate argument or carried on the existingConfigstruct.2. Jump hosts (first hop in scope, later hops out of scope)
The first hop goes through
JumpChain::connect_to_first_jump(src/jump/chain.rsline 350), which reaches the sameconnect_with_config_innerpath, so it is covered by fix 1.Later hops in
src/jump/chain/tunnel.rscallrussh::client::connect_stream(lines 122 and 234) on a stream that already exists (a channel through the previous hop). No local TCP connect happens there. The remote sshd performs the resolution, which bssh cannot influence.One nuance worth deciding on:
src/jump/chain/tunnel.rslines 97 and 220 do callto_socket_addrs().next(), but only to build aSocketAddrforClientHandler(host key verification context and display), not to open a socket. Taking.next()unconditionally means the family recorded there is also not controllable, so a mismatch between the recorded address and the actual connection could confuse known_hosts diagnostics.3. Port forwarding listeners (decision required)
src/forwarding/local.rsline 250:TcpListener::bind(self.bind_addr)src/forwarding/dynamic/forwarder.rsline 210:TcpListener::bind(self.bind_addr)bind_addrcurrently defaults toIpAddr::V4(Ipv4Addr::LOCALHOST)(src/forwarding/spec.rsline 55 forLocal, line 105 forRemote).In OpenSSH,
AddressFamilyalso constrains which addresses forwarded ports listen on. This issue must decide whether-6makes-L/-Dbind to::1instead of127.0.0.1.Related:
ChannelManager::open_direct_tcpip_channel(src/ssh/tokio_client/channel_manager.rsline 169) resolves the forwarding target locally and iterates candidates to send in thedirect-tcpiprequest. Whether the forced family should filter that list is a separate question, since the address is interpreted by the remote sshd rather than connected to locally.4.
bssh-server(out of scope)src/server/mod.rsline 277 (TcpListener::bind(addr)) belongs to thebssh-serverbinary, which has its own CLI insrc/bin/bssh_server.rs. Out of scope unless the server CLI grows the same flags.Decision points
Answer these before implementation:
-6change the default listen address for-L/-Dfrom127.0.0.1to::1? OpenSSH'sAddressFamilydoes constrain forwarding listeners, but changing this default is user-visible and could break scripts that assume IPv4 loopback. If yes, does an explicitbind_addressin the forwarding spec override the flag?open_direct_tcpip_channel, given that the remote sshd performs the actual connect?SocketAddrused forClientHandlerrespect the forced family, for consistency with host key records?no IPv6 address found for <host>, using the same non-zero exit path as other connection failures. Confirm the wording, and confirm it is a hard failure with no fallback to the other family (which is what OpenSSH does).AddressFamilyvalues. Acceptany,inet,inet6case-insensitively. Decide the behavior for an unrecognized value: hard parse error, or warn and treat asany.Acceptance criteria
Integration into the real code flow is required. A standalone helper function that nothing calls does not satisfy this issue.
-4and-6are read fromCliand threaded through toconnect_with_config_inner, so the flag demonstrably changes which address is connected to. Verified on a dual-stack host, or with a test double that asserts the family of the connectedSocketAddr.AddressFamily any|inet|inet6from SSH config is honored, with the command line flag taking precedence over the config keyword.could not resolve to any addresses.AddressFamilyis set: all resolved addresses are tried in resolver order.-4yields only IPv4 candidates,-6yields only IPv6, and neither flag yields the original list unchanged and in order.docs/man/bssh.1already documents-4at line 82 and-6at line 86 with the text "Force use of IPv4/IPv6 addresses only", so the man page currently describes behavior that does not exist. Update those entries with the actual scope once implemented (which paths are affected, the jump host limitation, forwarding behavior), and documentAddressFamilysupport.Notes
Found while refreshing #75. A proposed
network-pingcommand would be the natural first consumer of the address family preference, since connectivity testing is exactly where forcing a family matters most. Whoever picks up #75 should depend on the preference being plumbed through rather than reimplementing the filter locally.