Add ProxyCommand support and make ProxyJump actually work#16
Open
ArielTM wants to merge 2 commits into
Open
Conversation
Adds a new ProxyCommand field end-to-end and makes the existing
ProxyJump field actually do something at connect time (previously
stored and shown in the UI but never consulted by manager.rs).
Backend
- types/session.rs: HostConfig gains proxy_jump, proxy_command.
- db: migration 11->12 adds proxy_command column; SavedHost field
and every SQL site (insert/upsert/select for list_hosts, get_host,
test fixture) updated.
- ssh/commands.rs, portforward/commands.rs: forward both fields when
building HostConfig from SavedHost.
- ssh/manager.rs: extract open_authenticated_handle helper shared by
connect() and connect_no_pty(). Builds transport per OpenSSH
precedence: ProxyCommand wins, then ProxyJump, then direct TCP.
- ProxyCommand: russh_config::Stream::proxy_command run via
sh -c (Unix) / cmd /C (Windows) so quotes, pipes, env vars and
tilde all work. Tokens %h/%p/%r/%% expanded at connect time.
- ProxyJump: native single-hop via channel_open_direct_tcpip on
the bastion handle, then client::connect_stream over that
channel. Reuses the target's auth_method for the jump. The
bastion handle is kept alive in an AuthenticatedHandle.jump_chain
so dropping it doesn't tear down the transport. Multi-hop chains
are rejected with a clear pointer to ProxyCommand.
- parse_proxy_jump + expand_proxy_command_tokens with 7 unit tests.
- ssh/session.rs: SshSession carries the optional jump_chain anchor.
- import: enable ALLOW_UNSUPPORTED_FIELDS in ssh2-config and pull
ProxyCommand out of params.unsupported_fields["proxycommand"];
forward through SshConfigEntry/SshConfigImportEntry to SavedHost.
Frontend
- types/ssh.ts: proxy_command added to SavedHost, HostConfig,
SshConfigEntry.
- HostEditModal: replace the "TODO: Proxy / Jump Host" placeholder
with a real "Proxy / Jump host" section. Two inputs (ProxyJump,
ProxyCommand) with inline hints documenting single-hop, token
substitution, and OpenSSH precedence.
- ImportSshConfigModal: forward proxy_command on import.
Docs
- README: tighten the proxy bullet to "ProxyJump (single-hop bastion
host), and ProxyCommand" so docs match shipped behaviour.
Dependencies
- Cargo.toml: russh-config = "0.48" (standalone crate; no dep on
russh itself, so version-skew with russh 0.46 is fine).
- tests/proxy_command_import.rs: integration test that locks in the assumption our import path makes about ssh2-config 0.7 — that ProxyCommand lands in `params.unsupported_fields["proxycommand"]` (lowercased key, tokenised args) when parsed with ALLOW_UNSUPPORTED_FIELDS. Verified against a real-world style ssh_config snippet during dev. - pnpm-workspace.yaml: pnpm 11.x treats the IGNORED_BUILDS warning as a hard error inside its internal `runDepsStatusCheck` pre-flight, which kills `pnpm tauri dev` before vite/cargo even start. Approve esbuild's build script via `onlyBuiltDependencies` and disable the pre-flight check via `verifyDepsBeforeRun: false` so `tauri dev` works out-of-the-box on a fresh checkout.
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.
Summary
The README already advertises "proxy jump (bastion host)" support, but it turns out
manager.rsnever reads theproxy_jumpfield — the connection always goes direct TCP. So the field has been stored, imported from~/.ssh/config, and editable in the UI, but ignored at connect time. This PR wires it up for real and adds ProxyCommand alongside it.Highlights:
ProxyCommandfield, end-to-end (struct → DB migration → form → import → connect).channel_open_direct_tcpip, no shell-out to systemssh.connect) and SFTP (connect_no_pty) paths go through one sharedopen_authenticated_handlehelper, so SFTP gets proxy support automatically.ProxyCommanddirectives (it was already grabbingProxyJump).How it works
Transport selection (matches OpenSSH precedence: ProxyCommand wins if both are set):
%h/%p/%r/%%are expanded, then the string is handed tosh -c(Unix) /cmd /C(Windows) viarussh_config::Stream::proxy_command. Wrapping in a shell rather than callingStream's built-insplit(' ')keeps quotes, pipes, env vars and~expansion working — matters for anything copy-pasted from~/.ssh/config.[user@]host[:port], recursively authenticate to the bastion (reusing the target's auth method), open adirect-tcpipchannel to(target_host, target_port), then feedchannel.into_stream()torussh::client::connect_streamfor the target session. The bastion handle is kept alive inAuthenticatedHandle.jump_chain— dropping it would tear down the transport.Multi-hop ProxyJump (
a,b,c) is rejected with an error message pointing at ProxyCommand. ~95% of bastion setups are single-hop, and chains have an escape hatch viaProxyCommand ssh -J a,b,c -W %h:%p.UI
HostEditModalgains a "Proxy / Jump host" section with two inputs:[user@]host[:port]. Hint: "Single-hop only. Reuses this host's credentials to authenticate to the bastion."Why russh-config instead of building it ourselves
russh-config 0.48providesStream::tcp_connect/Stream::proxy_commandreturning anAsyncRead + AsyncWriteyou can feed straight torussh::client::connect_stream. No need to hand-roll subprocess piping. Note:russh-configitself doesn't handle ProxyJump — itsConfig::stream()only does ProxyCommand. The jump logic in this PR uses rawrusshchannels.The crate is added as a separate dep at
"0.48". It's standalone (no transitive dep onrussh), so the version skew with this project'srussh = "0.46"doesn't matter.Why native ProxyJump and not shell out to
ssh -JShelling out to system
sshwould have been ~2 lines (just feed it through ProxyCommand), but:~/.ssh/, leading to "works in Terminal but not in anyscp" support tickets.Files
src-tauri/Cargo.tomlrussh-config = "0.48"src-tauri/src/types/session.rsHostConfiggainsproxy_jump,proxy_commandsrc-tauri/src/db/mod.rsproxy_commandcolumn;SavedHostfield + every SQL sitesrc-tauri/src/ssh/manager.rsopen_authenticated_handle; transport builder; token expansion; jump-chain anchoringsrc-tauri/src/ssh/session.rsSshSessioncarries the optionaljump_chainanchorsrc-tauri/src/ssh/commands.rs,portforward/commands.rsHostConfigsrc-tauri/src/import/mod.rsALLOW_UNSUPPORTED_FIELDS; extract ProxyCommand fromunsupported_fields["proxycommand"]src-tauri/src/import/commands.rsproxy_commandthrough toSavedHostsrc-tauri/tests/proxy_command_import.rssrc/types/ssh.tssrc/components/dashboard/HostEditModal.tsxsrc/components/dashboard/ImportSshConfigModal.tsxproxy_commandon importREADME.mdpnpm-workspace.yamlpnpm tauri deverrors on a fresh checkout)Test plan
cargo checkcleancargo test --lib— 7 new unit tests forexpand_proxy_command_tokensandparse_proxy_jump, all passcargo test --test proxy_command_import— verifies ssh2-config behaviour our extractor depends ontsc --noEmitclean11→12ran against an existinganyscp.dbwithout losing data~/.ssh/configwithProxyCommand /path/to/script %h %p— value appears in the DB and the host edit modalThings that are intentionally out of scope: