Connect to remote daemons over SSH (#3989)

Add transport-only SSH connectivity for the CLI and Electron desktop through the user's OpenSSH configuration. Remote installation and daemon lifecycle remain explicit, separate operations.

Preserve actionable SSH failure details when the proxy socket closes before the SSH child exits.

Closes #3949

Co-authored-by: James Strain <jstrain@nvidia.com>
Co-authored-by: Reid Levesque <reidlevesque@users.noreply.github.com>
This commit is contained in:
Mohamed Boudra
2026-08-28 16:32:12 +02:00
committed by GitHub
co-authored by James Strain Reid Levesque
parent 7ea4288117
commit 667664847d
38 changed files with 1961 additions and 251 deletions
@@ -0,0 +1,66 @@
import { describe, expect, it } from "vitest";
import { buildSshTunnelArgs, parseSshTransportUri, validateSshHost } from "./ssh-transport.js";
describe("SSH transport", () => {
it("parses SSH targets with Paseo's default daemon port", () => {
expect(parseSshTransportUri("ssh://deploy@example.com:2222")).toEqual({
host: "deploy@example.com",
sshPort: 2222,
daemonPort: 6767,
});
});
it("accepts an explicit remote daemon port", () => {
expect(parseSshTransportUri("ssh://build-box?daemonPort=7777")).toEqual({
host: "build-box",
daemonPort: 7777,
});
});
it("passes IPv6 hosts to OpenSSH without URI brackets", () => {
expect(parseSshTransportUri("ssh://deploy@[2001:db8::1]:2222")).toEqual({
host: "deploy@2001:db8::1",
sshPort: 2222,
daemonPort: 6767,
});
});
it.each([
"http://build-box",
"ssh://build-box/path",
"ssh://build-box?unknown=true",
"ssh://build-box?daemonPort=0",
"ssh://user:secret@build-box",
])("rejects invalid target %s", (target) => {
expect(() => parseSshTransportUri(target)).toThrow();
});
it("builds a non-interactive stdio tunnel and preserves SSH config", () => {
expect(
buildSshTunnelArgs({
host: "deploy@build-box",
sshPort: 2222,
daemonPort: 7777,
}),
).toEqual([
"-T",
"-o",
"BatchMode=yes",
"-o",
"ConnectTimeout=10",
"-o",
"ClearAllForwardings=yes",
"-o",
"ExitOnForwardFailure=yes",
"-p",
"2222",
"-W",
"127.0.0.1:7777",
"deploy@build-box",
]);
});
it.each(["", "-oProxyCommand=bad", "bad host"])("rejects unsafe SSH host %j", (host) => {
expect(() => validateSshHost(host)).toThrow();
});
});
+82
View File
@@ -0,0 +1,82 @@
export const DEFAULT_SSH_DAEMON_PORT = 6767;
export interface SshTransportTarget {
host: string;
sshPort?: number;
daemonPort: number;
}
export function validatePort(value: string | number, label: string): number {
const port = Number(value);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
throw new Error(`${label} must be between 1 and 65535`);
}
return port;
}
export function validateSshHost(host: string): string {
const normalized = host.trim();
if (!normalized) throw new Error("SSH host is required");
if (/\s/u.test(normalized) || normalized.startsWith("-")) {
throw new Error("SSH host is invalid");
}
return normalized;
}
export function parseSshTransportUri(value: string): SshTransportTarget {
let url: URL;
try {
url = new URL(value);
} catch (error) {
throw new Error("Invalid SSH host URI", { cause: error });
}
if (url.protocol !== "ssh:" || url.password || (url.pathname !== "" && url.pathname !== "/")) {
throw new Error("Invalid SSH host URI");
}
if (url.hash) throw new Error("SSH host URI does not support fragments");
for (const key of url.searchParams.keys()) {
if (key !== "daemonPort") throw new Error(`Unsupported SSH host option: ${key}`);
}
const daemonPorts = url.searchParams.getAll("daemonPort");
if (daemonPorts.length > 1) throw new Error("daemonPort may only be specified once");
const urlHostname = url.hostname;
const hostname = validateSshHost(
urlHostname.startsWith("[") && urlHostname.endsWith("]")
? urlHostname.slice(1, -1)
: urlHostname,
);
const username = decodeURIComponent(url.username);
const host = validateSshHost(username ? `${username}@${hostname}` : hostname);
return {
host,
...(url.port ? { sshPort: validatePort(url.port, "SSH port") } : {}),
daemonPort:
daemonPorts[0] === undefined
? DEFAULT_SSH_DAEMON_PORT
: validatePort(daemonPorts[0], "Daemon port"),
};
}
export function buildSshTunnelArgs(target: SshTransportTarget): string[] {
const host = validateSshHost(target.host);
const daemonPort = validatePort(target.daemonPort, "Daemon port");
const args = [
"-T",
"-o",
"BatchMode=yes",
"-o",
"ConnectTimeout=10",
"-o",
"ClearAllForwardings=yes",
"-o",
"ExitOnForwardFailure=yes",
];
if (target.sshPort !== undefined) {
args.push("-p", String(validatePort(target.sshPort, "SSH port")));
}
args.push("-W", `127.0.0.1:${daemonPort}`, host);
return args;
}