Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,6 @@ russh = "0.45"
russh-sftp = "2.1"
russh-keys = "0.45"
shellexpand = "3"
ssh_config = "0.1"
rustls = { version = "0.23", default-features = false }

[patch.crates-io]
Expand Down
39 changes: 39 additions & 0 deletions docs/architecture/remote-workspace-transport.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,45 @@ unsupported by the Product Operation Registry. Existing Remote Control sessions
reuse the bound workspace providers, and Detached Dispatch does not create WSL
profiles. Native WSL has no SSH transport for port forwarding.

## ProxyCommand

The additive `proxyCommand` profile field carries the SSH transport over a
local process's stdin/stdout, as OpenSSH does (for example
`coder ssh --stdio %h`). The Services layer and the port-forward tool's
`~/.ssh/config` path honor it; the desktop connection dialog does not send
`proxyCommand` yet, so profiles created there still dial directly. When such a
profile targets a `~/.ssh/config` alias that has a ProxyCommand, the host logs
a warning and a failed dial names the ignored ProxyCommand instead of
reporting only a network error. Missing `proxyCommand` preserves direct
dialing; connection drift includes the field.

`%h`, `%p`, `%r`, and `%%` are expanded first, and a host or user that would be
substituted must match `[A-Za-z0-9._\-@:%\[\]]` (OpenSSH 9.6, CVE-2023-51385)
or the connection fails before anything runs. Unix runs `sh -c "exec …"` in its
own process group; Windows runs `cmd.exe /d /v:off /s /c "…"` inside a
kill-on-close Job. Disconnect, failed handshakes, and reconnect tear the whole
process tree down; a reconnect keeps a displaced proxy only until its old
transport has no users left.

A saved profile with both ProxyCommand and ProxyJump, or with a container or
WSL target, is rejected. A ProxyJump hop whose `~/.ssh/config` alias resolves
to a ProxyCommand fails with an explicit unsupported error instead of dialing
the hop directly.

`~/.ssh/config` values come from a line-based resolver over the
`Include`-merged text. Include runs inside the includer's `Host`/`Match`
context, which is restored afterwards. Host patterns support `*`, `?`, `!`,
and inline `#` comments. The first value wins, except that `IdentityFile` and
`CertificateFile` accumulate; single-key consumers take the first value from a
block that names the host exactly. `ProxyCommand` is kept verbatim (`none`
disables it), and ProxyJump and ProxyCommand compete: whichever is obtained
first wins.

The host advertises `ssh_proxy_command_v1`. Older hosts ignore the unknown
field and would dial directly, so a future controller UI must not send
`proxyCommand` to a peer that lacks the capability. CLI peer setup is
unsupported, as for WSL.

## ProxyJump

The comma-separated jump chain is resolved left to right. Each token can be a
Expand Down
1 change: 0 additions & 1 deletion scripts/core-boundaries/rules/feature-rules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,6 @@ export const optionalDependencyFeatureOwnerRules = [
{ depName: 'sherpa-onnx', ownerFeatures: ['speech'] },
{ depName: 'shellexpand', ownerFeatures: ['remote-ssh-concrete'] },
{ depName: 'sse-stream', ownerFeatures: ['mcp'] },
{ depName: 'ssh_config', ownerFeatures: ['remote-ssh-concrete', 'ssh_config'] },
{ depName: 'terminal-core', ownerFeatures: ['remote-ssh', 'remote-ssh-concrete'] },
{ depName: 'tar', ownerFeatures: ['speech'] },
{ depName: 'thiserror', ownerFeatures: ['account-identity', 'browser-control', 'git', 'hook-import', 'miniapp-market', 'plugin-source', 'remote-connect', 'remote-ssh', 'remote-ssh-concrete', 'review-platform', 'speech', 'web-tools', 'workspace-search'] },
Expand Down
1 change: 1 addition & 0 deletions src/apps/desktop/src/api/ssh_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1506,6 +1506,7 @@ mod tests {
},
default_workspace: Some("/workspace".to_string()),
proxy_jump: None,
proxy_command: None,
container: Some(ContainerWorkspaceConfig {
name: "dev".to_string(),
access: ContainerAccess::DockerExec,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,10 @@ impl PortForwardTool {
username,
auth,
default_workspace: None,
proxy_jump: config_entry.and_then(|entry| entry.proxy_jump),
proxy_jump: config_entry
.as_ref()
.and_then(|entry| entry.proxy_jump.clone()),
proxy_command: config_entry.and_then(|entry| entry.proxy_command),
container: None,
wsl: None,
options: SSHConnectionOptions::default(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"digest": "fnv1a64:6af8cd66ba85f89e",
"digest": "fnv1a64:378e125752d83ce2",
"retiredCommandPrefixes": [
{
"prefix": "lsp_",
Expand Down Expand Up @@ -33,6 +33,7 @@
"miniapp_agent_context_files_v1",
"miniapp_agent_images_v1",
"wsl_workspaces_v1",
"ssh_proxy_command_v1",
"computer_use_sharing_v1",
"product_control_v1",
"remote_connect_control_v1",
Expand Down Expand Up @@ -76,6 +77,7 @@
"miniapp_agent_context_files_v1",
"miniapp_agent_images_v1",
"wsl_workspaces_v1",
"ssh_proxy_command_v1",
"computer_use_sharing_v1",
"product_control_v1",
"remote_connect_control_v1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ pub enum PeerHostCapability {
MiniappAgentImagesV1,
/// WSL profiles and discovery are understood; availability is probed on the host.
WslWorkspacesV1,
/// Saved SSH profiles may carry `proxyCommand`; the host runs it as the
/// SSH transport. Older hosts drop the field and dial directly.
SshProxyCommandV1,
/// User-selected native window sharing and explicit permission receipts.
ComputerUseSharingV1,
/// `product_control_invoke` is implemented (shared config contract).
Expand Down Expand Up @@ -133,6 +136,7 @@ impl PeerHostCapability {
Self::MiniappAgentContextFilesV1,
Self::MiniappAgentImagesV1,
Self::WslWorkspacesV1,
Self::SshProxyCommandV1,
Self::ComputerUseSharingV1,
Self::ProductControlV1,
Self::RemoteConnectControlV1,
Expand Down Expand Up @@ -180,6 +184,7 @@ impl PeerHostCapability {
Self::MiniappAgentContextFilesV1 => "miniapp_agent_context_files_v1",
Self::MiniappAgentImagesV1 => "miniapp_agent_images_v1",
Self::WslWorkspacesV1 => "wsl_workspaces_v1",
Self::SshProxyCommandV1 => "ssh_proxy_command_v1",
Self::ComputerUseSharingV1 => "computer_use_sharing_v1",
Self::ProductControlV1 => "product_control_v1",
Self::RemoteConnectControlV1 => "remote_connect_control_v1",
Expand Down Expand Up @@ -212,7 +217,7 @@ const DESKTOP_CAPABILITIES: &[PeerHostCapability] = PeerHostCapability::ALL
.split_at(PeerHostCapability::ALL.len() - 1)
.0;

/// The CLI peer host has no MiniApp runtime, WSL connection setup, host-native
/// The CLI peer host has no MiniApp runtime, WSL or SSH connection setup, host-native
/// ProductControl providers, or presentation surface.
const CLI_CAPABILITIES: &[PeerHostCapability] = &[
PeerHostCapability::ModelRoutingV1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1201,6 +1201,7 @@ mod tests {
default_workspace: Some("/srv/project".to_string()),
last_connected: Some(1),
proxy_jump: Some("jump.example.invalid".to_string()),
proxy_command: None,
container: None,
options: owner::SshConnectionOptionsRecord::default(),
}
Expand Down
4 changes: 2 additions & 2 deletions src/crates/services/services-integrations/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ chrono = { workspace = true, optional = true }
dunce = { workspace = true, optional = true }
futures = { workspace = true, optional = true }
futures-util = { workspace = true, optional = true }
glob = { workspace = true, optional = true }
fs2 = { workspace = true, optional = true }
git2 = { workspace = true, optional = true }
hostname = { workspace = true, optional = true }
Expand Down Expand Up @@ -75,7 +76,6 @@ russh = { workspace = true, optional = true }
russh-sftp = { workspace = true, optional = true }
russh-keys = { workspace = true, optional = true }
shellexpand = { workspace = true, optional = true }
ssh_config = { workspace = true, optional = true }
terminal-core = { path = "../terminal", optional = true }
x25519-dalek = { workspace = true, optional = true }

Expand Down Expand Up @@ -511,7 +511,7 @@ product-full = [
"workspace-search",
]
skillhub = ["openbitfun-services-core/tls-provider", "reqwest", "reqwest/json", "reqwest/query", "reqwest/rustls-no-provider", "reqwest/stream", "zip", "tempfile", "tokio/fs", "tokio/io-util"]
ssh_config = ["dep:ssh_config"]
ssh_config = ["dep:glob"]

[dev-dependencies]
tempfile = { workspace = true }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,8 @@ pub struct SavedConnectionRecord {
pub last_connected: Option<u64>,
#[serde(default)]
pub proxy_jump: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proxy_command: Option<String>,
#[serde(default)]
pub container: Option<ContainerWorkspaceRecord>,
#[serde(default)]
Expand Down
Loading
Loading