Skip to content

feat(remote-ssh): carry SSH transports over ProxyCommand processes - #3591

Open
Raing5Days wants to merge 1 commit into
GCWing:mainfrom
Raing5Days:feat/ssh-proxy-command
Open

Raing5Days wants to merge 1 commit into
GCWing:mainfrom
Raing5Days:feat/ssh-proxy-command

Conversation

@Raing5Days

Copy link
Copy Markdown
Contributor

Summary

Adds OpenSSH-style ProxyCommand support to the remote SSH service (first batch, backend only), so SSH remote workspaces can follow Coder-style platform configs (coder config-ssh) where the SSH transport is a local process's stdin/stdout and the workspace itself runs no sshd.

  • Data model: optional proxyCommand threaded through SSHConnectionConfig, SSHConfigEntry, SavedConnection, and SavedConnectionRecord with #[serde(default)]; older payloads and connections.json files stay loadable, and connection_params_equal treats a changed command as config drift.
  • Transport: the proxy command runs through process_manager::create_tokio_command (cmd.exe /C with raw args on Windows, sh -c elsewhere) after %h/%p/%r/%% expansion; the SSH handshake reuses the existing establish_stream_session, so handshake timeouts, known-hosts verification, and authentication are unchanged.
  • Lifecycle: the child process is owned for the whole session (unpublished-handshake guard, published ActiveConnection, disconnect/reconnect/test paths all kill it). Failures report the proxy exit code plus the last stderr lines instead of a bare handshake error. A reconnect keeps a stale proxy alive only while its old transport still serves in-flight work.
  • ~/.ssh/config: Include is now expanded before parsing (recursive, glob, ~, cycle- and depth-guarded), so hosts that only exist in included files (Coder, AtomGit, devenv) are listed and resolved. In this release ProxyCommand is exclusive with ProxyJump and Docker container targets; combinations are rejected with explicit errors.

No UI yet by design: the connection-dialog field lands in a follow-up batch. The field already flows end to end from ~/.ssh/config resolution, saved configs, and wire payloads.

Type and Areas

Type: Feature

Areas: Rust services (services-integrations remote_ssh / remote_persistence), legacy migration adapters

Motivation / Impact

OpenBitFun could only connect over direct TCP (plus ProxyJump chains), and ssh_config 0.1 neither understands ProxyCommand nor expands Include, so Coder-style hosts were silently treated as normal direct hosts and included Host blocks were invisible. With this change a saved config (or ~/.ssh/config host) carrying ProxyCommand connects through the proxy process, and included Host blocks show up in the host list. Pod rebuilds on such platforms no longer require re-provisioning an sshd.

Verification

  • cargo test -p openbitfun-services-integrations --no-default-features --features remote-ssh-concrete --lib remote_ssh:: — 159 passed / 0 failed (includes new ProxyCommand token/lifecycle/exclusivity tests and Include loader tests)
  • cargo test -p openbitfun-services-integrations --no-default-features --features remote-ssh --test remote_ssh_contracts — 18 passed / 0 failed; same target under --features remote-ssh-concrete — 17 passed / 0 failed
  • cargo test -p openbitfun-services-integrations --no-default-features --features remote-persistence --lib remote_persistence::tests:: — 4 passed / 0 failed
  • cargo clippy -p openbitfun-services-integrations --no-default-features --features remote-ssh-concrete --lib --tests — no warnings in the new code
  • cargo check -p openbitfun-legacy-migration-adapters --lib --tests — passed
  • node scripts/check-core-boundaries.mjs — passed
  • node scripts/format-changed-rust.mjs — clean
  • Not yet exercised against a live Coder workspace (needs the UI batch plus a real deployment); the process lifecycle is covered by real child-process unit tests instead.

Reviewer Notes

  • Remote scenarios: this is the Remote Workspace path. The persisted/wire shapes only gain an optional field, so Detached Dispatch and Peer Device hosts on older builds keep reading records written by newer ones and vice versa. The proxy command executes on whichever host establishes the SSH session, matching OpenSSH semantics.
  • Compatibility: the new field is optional on read and omitted on write when absent; no persisted shape is repurposed or narrowed.
  • Deliberate exclusions (explicit errors, not silent behavior): ProxyCommand + ProxyJump, ProxyCommand + container targets, WSL + ProxyCommand.
  • Host keys: the target stays the ~/.ssh/config alias in OpenBitFun's known-hosts store; Coder workspaces that regenerate host keys will surface the app's own host-key verification on reconnect. StrictHostKeyChecking remains unparsed.

Generated with OpenBitFun

Add OpenSSH-style ProxyCommand support to the remote SSH service, first
batch (backend only):

- Thread an optional proxyCommand through SSHConnectionConfig,
  SSHConfigEntry, SavedConnection, and SavedConnectionRecord with serde
  defaults so older payloads and connections.json stay loadable, and
  include it in connection drift detection.
- Spawn the ProxyCommand through the shared process manager (cmd.exe /C
  with raw args on Windows, sh -c elsewhere), expand %h/%p/%r/%% tokens,
  drain stderr into the log plus a bounded tail, and carry the SSH
  handshake over the joined stdout/stdin via the existing
  establish_stream_session path.
- Own the child process for the whole session lifetime: unpublished
  handshakes, published connections, and disconnect/reconnect paths all
  kill it, and a reconnect keeps a stale proxy alive only while its old
  transport still serves in-flight work. Failures report the proxy exit
  code and stderr tail instead of a bare handshake error.
- Merge ~/.ssh/config with its Include tree (recursive, glob, cycle and
  depth guarded) before parsing, making Coder/AtomGit style hosts
  visible; ProxyCommand stays exclusive with ProxyJump and container
  targets, rejected with explicit errors for now.

The connection dialog field for editing proxyCommand lands in a
follow-up batch; ~/.ssh/config hosts already carry it end to end.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant