Skip to content

Commit

Permalink
Use coder ssh in place of coder vscodessh (#420)
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronlehmann authored Feb 4, 2025
1 parent 4dd0d70 commit 9253a22
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 20 deletions.
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ contains the `coder-vscode` prefix, and if so we delay activation to:

```text
Host coder-vscode.dev.coder.com--*
ProxyCommand "/tmp/coder" vscodessh --network-info-dir "/home/kyle/.config/Code/User/globalStorage/coder.coder-remote/net" --session-token-file "/home/kyle/.config/Code/User/globalStorage/coder.coder-remote/dev.coder.com/session_token" --url-file "/home/kyle/.config/Code/User/globalStorage/coder.coder-remote/dev.coder.com/url" %h
ProxyCommand "/tmp/coder" --global-config "/home/kyle/.config/Code/User/globalStorage/coder.coder-remote/dev.coder.com" ssh --stdio --network-info-dir "/home/kyle/.config/Code/User/globalStorage/coder.coder-remote/net" --ssh-host-prefix coder-vscode.dev.coder.com-- %h
ConnectTimeout 0
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
Expand All @@ -50,8 +50,8 @@ specified port. This port is printed to the `Remote - SSH` log file in the VS
Code Output panel in the format `-> socksPort <port> ->`. We use this port to
find the SSH process ID that is being used by the remote session.

The `vscodessh` subcommand on the `coder` binary periodically flushes its
network information to `network-info-dir + "/" + process.ppid`. SSH executes
The `ssh` subcommand on the `coder` binary periodically flushes its network
information to `network-info-dir + "/" + process.ppid`. SSH executes
`ProxyCommand`, which means the `process.ppid` will always be the matching SSH
command.

Expand Down
2 changes: 1 addition & 1 deletion src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ async function openWorkspace(
// when opening a workspace unless explicitly specified.
let remoteAuthority = `ssh-remote+${AuthorityPrefix}.${toSafeHost(baseUrl)}--${workspaceOwner}--${workspaceName}`
if (workspaceAgent) {
remoteAuthority += `--${workspaceAgent}`
remoteAuthority += `.${workspaceAgent}`
}

let newWindow = true
Expand Down
2 changes: 2 additions & 0 deletions src/featureSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as semver from "semver"
export type FeatureSet = {
vscodessh: boolean
proxyLogDirectory: boolean
wildcardSSH: boolean
}

/**
Expand All @@ -21,5 +22,6 @@ export function featureSetForVersion(version: semver.SemVer | null): FeatureSet
// If this check didn't exist, VS Code connections would fail on
// older versions because of an unknown CLI argument.
proxyLogDirectory: (version?.compare("2.3.3") || 0) > 0 || version?.prerelease[0] === "devel",
wildcardSSH: (version?.compare("2.19.0") || 0) > 0 || version?.prerelease[0] === "devel",
}
}
42 changes: 32 additions & 10 deletions src/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,20 +467,27 @@ export class Remote {
// "Host not found".
try {
this.storage.writeToCoderOutputChannel("Updating SSH config...")
await this.updateSSHConfig(workspaceRestClient, parts.label, parts.host, binaryPath, logDir)
await this.updateSSHConfig(workspaceRestClient, parts.label, parts.host, binaryPath, logDir, featureSet)
} catch (error) {
this.storage.writeToCoderOutputChannel(`Failed to configure SSH: ${error}`)
throw error
}

// TODO: This needs to be reworked; it fails to pick up reconnects.
this.findSSHProcessID().then((pid) => {
this.findSSHProcessID().then(async (pid) => {
if (!pid) {
// TODO: Show an error here!
return
}
disposables.push(this.showNetworkUpdates(pid))
this.commands.workspaceLogPath = logDir ? path.join(logDir, `${pid}.log`) : undefined
if (logDir) {
const logFiles = await fs.readdir(logDir)
this.commands.workspaceLogPath = logFiles
.reverse()
.find((file) => file === `${pid}.log` || file.endsWith(`-${pid}.log`))
} else {
this.commands.workspaceLogPath = undefined
}
})

// Register the label formatter again because SSH overrides it!
Expand Down Expand Up @@ -532,7 +539,14 @@ export class Remote {

// updateSSHConfig updates the SSH configuration with a wildcard that handles
// all Coder entries.
private async updateSSHConfig(restClient: Api, label: string, hostName: string, binaryPath: string, logDir: string) {
private async updateSSHConfig(
restClient: Api,
label: string,
hostName: string,
binaryPath: string,
logDir: string,
featureSet: FeatureSet,
) {
let deploymentSSHConfig = {}
try {
const deploymentConfig = await restClient.getDeploymentSSHConfig()
Expand Down Expand Up @@ -610,13 +624,21 @@ export class Remote {
headerArg = ` --header-command ${escapeSubcommand(headerCommand)}`
}

const hostPrefix = label ? `${AuthorityPrefix}.${label}--` : `${AuthorityPrefix}--`

const proxyCommand = featureSet.wildcardSSH
? `${escape(binaryPath)}${headerArg} --global-config ${escape(
path.dirname(this.storage.getSessionTokenPath(label)),
)} ssh --stdio --network-info-dir ${escape(this.storage.getNetworkInfoPath())}${await this.formatLogArg(logDir)} --ssh-host-prefix ${hostPrefix} %h`
: `${escape(binaryPath)}${headerArg} vscodessh --network-info-dir ${escape(
this.storage.getNetworkInfoPath(),
)}${await this.formatLogArg(logDir)} --session-token-file ${escape(this.storage.getSessionTokenPath(label))} --url-file ${escape(
this.storage.getUrlPath(label),
)} %h`

const sshValues: SSHValues = {
Host: label ? `${AuthorityPrefix}.${label}--*` : `${AuthorityPrefix}--*`,
ProxyCommand: `${escape(binaryPath)}${headerArg} vscodessh --network-info-dir ${escape(
this.storage.getNetworkInfoPath(),
)}${await this.formatLogArg(logDir)} --session-token-file ${escape(this.storage.getSessionTokenPath(label))} --url-file ${escape(
this.storage.getUrlPath(label),
)} %h`,
Host: hostPrefix + `*`,
ProxyCommand: proxyCommand,
ConnectTimeout: "0",
StrictHostKeyChecking: "no",
UserKnownHostsFile: "/dev/null",
Expand Down
7 changes: 7 additions & 0 deletions src/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ it("should parse authority", async () => {
username: "foo",
workspace: "bar",
})
expect(parseRemoteAuthority("vscode://ssh-remote+coder-vscode.dev.coder.com--foo--bar.baz")).toStrictEqual({
agent: "baz",
host: "coder-vscode.dev.coder.com--foo--bar.baz",
label: "dev.coder.com",
username: "foo",
workspace: "bar",
})
})

it("escapes url host", async () => {
Expand Down
23 changes: 17 additions & 6 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@ export function parseRemoteAuthority(authority: string): AuthorityParts | null {
// The authority looks like: vscode://ssh-remote+<ssh host name>
const authorityParts = authority.split("+")

// We create SSH host names in one of two formats:
// coder-vscode--<username>--<workspace>--<agent?> (old style)
// coder-vscode.<label>--<username>--<workspace>--<agent>
// We create SSH host names in a format matching:
// coder-vscode(--|.)<username>--<workspace>(--|.)<agent?>
// The agent can be omitted; the user will be prompted for it instead.
// Anything else is unrelated to Coder and can be ignored.
const parts = authorityParts[1].split("--")
Expand All @@ -38,15 +37,27 @@ export function parseRemoteAuthority(authority: string): AuthorityParts | null {
// Validate the SSH host name. Including the prefix, we expect at least
// three parts, or four if including the agent.
if ((parts.length !== 3 && parts.length !== 4) || parts.some((p) => !p)) {
throw new Error(`Invalid Coder SSH authority. Must be: <username>--<workspace>--<agent?>`)
throw new Error(`Invalid Coder SSH authority. Must be: <username>--<workspace>(--|.)<agent?>`)
}

let workspace = parts[2]
let agent = ""
if (parts.length === 4) {
agent = parts[3]
} else if (parts.length === 3) {
const workspaceParts = parts[2].split(".")
if (workspaceParts.length === 2) {
workspace = workspaceParts[0]
agent = workspaceParts[1]
}
}

return {
agent: parts[3] ?? "",
agent: agent,
host: authorityParts[1],
label: parts[0].replace(/^coder-vscode\.?/, ""),
username: parts[1],
workspace: parts[2],
workspace: workspace,
}
}

Expand Down

0 comments on commit 9253a22

Please sign in to comment.