fix(ssh): connect ProxyJump hosts through their bastion - #75
Merged
Conversation
The ssh_config parser read ProxyJump into `Host.proxy_jump`, but nothing downstream used it: the terminal refused to open such a host outright, while metrics, SFTP, snippets and key setup dialled the target address directly — past the bastion that was the only route to it. `connect_and_auth` now walks the jump chain the way `ssh -J` does. Each hop is connected and authenticated in turn, and the next one rides a `direct-tcpip` channel opened on its predecessor. The new `SshConnection` owns the bastion handles alongside the target's, since dropping one would tear down every tunnel above it; it derefs to the target handle, so callers open channels exactly as before. Chain resolution lives in the new `ssh::jump` module — pure and I/O-free, so it is unit-tested without a filesystem. A jump alias is looked up in the merged host list and inherits that entry's HostName, User, Port and IdentityFile; an alias matching no entry is used as a literal hostname. Multi-hop values, inline `user@host:port` overrides, IPv6 literals, bastions behind bastions and the `ProxyJump none` opt-out all follow OpenSSH. Cycles and chains past ten hops are reported rather than looped over. Also stops the TUI edit form from dropping ProxyJump: it has no field for it, so saving an imported host used to lose the bastion. The GUI already preserved it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
ProxyJumpis parsed from~/.ssh/configintoHost.proxy_jump, but nothing downstream acts on it:"ProxyJump is not yet supported in the terminal",pty.rs);connect_and_auth, which opens a TCP connection straight tohostname:port— past the bastion that is the only route to an internal host.So a config like this cannot be used at all: the terminal tab errors out and the dashboard card sits on a failed connection.
The 1.1.0 changelog records this as a known gap ("not yet supported in the terminal … Tracked as a follow-up").
Solution
connect_and_authnow walks the jump chain the wayssh -Jdoes: each hop is connected and authenticated in turn, and the next one rides adirect-tcpipchannel opened on its predecessor. Since every native SSH path already shares this function, the terminal, metrics, SFTP, snippets and key setup are all fixed at once.ssh::jumpmodule — pure, I/O-free chain resolution, so it unit-tests without a filesystem. A jump alias is looked up in the merged host list (hosts.toml+~/.ssh/config) and inherits that entry'sHostName,User,PortandIdentityFile; an alias matching no entry is used as a literal hostname. Multi-hop values (first,second), inlineuser@host:portoverrides, IPv6 literals, bastions behind bastions and theProxyJump noneopt-out all follow OpenSSH. Cycles and chains past ten hops are reported instead of looping.SshConnection— owns the bastion handles alongside the target's, because dropping one would tear down every tunnel above it. It derefs to the targetHandle, so all existing callers open channels unchanged.known_hostsunder its own name and authenticates with the usual agent → identity file → default keys → password order.ProxyJump— the form has no field for it, so saving an imported host used to lose the bastion. The GUI already preserved it (commands/hosts.rs).No new dependencies; the tunnel uses russh's own
channel_open_direct_tcpip+connect_stream.Test plan
cargo test— green. 20 new unit tests inssh::jumpcover spec parsing (bare alias,user@host:port, multi-hop, IPv6 bracketed and bare, unusable hops), resolution against known hosts, inline overrides, theHostName-less fallback, nested bastions,none, cycles (by alias and by address) and the hop cap. One test drives the whole path the reported config takes, fromparse_ssh_configthroughresolve_chain.cargo clippy -- -D warningsandcargo fmt --check— clean, for the workspace and for-p omnyssh-gui.ProxyJump public-proxyconnects in the GUI (cargo tauri dev) where it previously failed.ProxyJumptake the same code path as before — the host list is not even loaded for them.Notes for review
config::load_all_hosts(off the async worker, and only whenproxy_jumpis set). Happy to thread the list in from the app layer instead if you'd rather the engine not read config here.## Unreleasedheading — say the word if you'd prefer it folded into a version section.ProxyJumpfield in the add/edit form, so a bastion can only arrive via~/.ssh/configimport. That felt like a separate feature; glad to add it here if you want it in the same PR.