mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 08:02:32 +00:00
* feat(ssh): add ControlMaster multiplexing for system SSH transport System SSH transport spawns a new OpenSSH process per exec command (platform detect, relay install check, node resolution, relay launch, socket probe). Each process pays the full SSH handshake cost — ~9s on Uber devpods — making a typical relay connect take 54s+ and reliably exceeding the 15s startup reconnect budget. Add SSH ControlMaster multiplexing via a per-target socket in $TMPDIR/orca-ssh-ctl/<hash>.sock. The first command establishes the master; subsequent commands reuse it at ~100ms per exec instead of ~9s. ControlPersist=300 keeps the master alive after commands exit so rapid reconnects (e.g. on tab focus) also benefit. Windows is excluded since OpenSSH's ControlMaster support there is limited. * fix(ssh): address ControlMaster key collision and directory permission risks - Use target.id in the socket key so distinct SSH targets can never collide even when configHost/port/user happen to match - Switch from SHA1 to SHA256 and extend hash slice from 12 to 16 chars - Stat the control-socket directory after mkdirSync to reject pre-existing dirs that are symlinks, foreign-owned, or have group/other write bits (mkdirSync mode is ignored on pre-existing dirs) - Update two tests that used exact spawn-arg arrays; replace with ordering assertions (forward flags before --) that stay correct regardless of which extra ControlMaster options are injected * fix(ssh): bind ControlPath identity to route and reject symlinked ctl dir Fold proxyCommand/jumpHost/identity fields into the ControlPath hash so a target whose route is edited no longer reuses a still-alive master built on the old route. Switch the control-socket dir check from statSync to lstatSync so a planted symlink fails the directory validation outright. * test(ssh): drop tautological argv re-assertion in spawn checks The toHaveBeenCalledWith re-passed the args array extracted from the same mock call, making that argument position always pass. argv content is already verified by the index-ordering assertions above; use expect.any(Array) so the spawn check only claims what it actually verifies (binary path, stdio). * fix(ssh): harden system ssh connection reuse Co-authored-by: Orca <help@stably.ai> * test(ssh): isolate control socket runtime dir Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Test <test@example.com> Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com> Co-authored-by: Orca <help@stably.ai>
128 lines
3.7 KiB
TypeScript
128 lines
3.7 KiB
TypeScript
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
|
import type { SshTarget } from '../../shared/ssh-types'
|
|
|
|
const { lstatSyncMock, mkdirSyncMock, tmpdirMock } = vi.hoisted(() => ({
|
|
lstatSyncMock: vi.fn(),
|
|
mkdirSyncMock: vi.fn(),
|
|
tmpdirMock: vi.fn()
|
|
}))
|
|
|
|
vi.mock('node:fs', async (importOriginal) => {
|
|
const actual = (await importOriginal()) as Record<string, unknown>
|
|
return {
|
|
...actual,
|
|
lstatSync: lstatSyncMock,
|
|
mkdirSync: mkdirSyncMock
|
|
}
|
|
})
|
|
|
|
vi.mock('node:os', async (importOriginal) => {
|
|
const actual = (await importOriginal()) as Record<string, unknown>
|
|
return {
|
|
...actual,
|
|
tmpdir: tmpdirMock
|
|
}
|
|
})
|
|
|
|
import { getControlSocketPath, type SystemSshResolvedConfig } from './ssh-control-socket'
|
|
|
|
const CURRENT_UID = process.getuid?.() ?? 501
|
|
|
|
function createTarget(overrides?: Partial<SshTarget>): SshTarget {
|
|
return {
|
|
id: 'target-1',
|
|
label: 'Test Server',
|
|
configHost: 'devpod',
|
|
host: '10.0.0.5',
|
|
port: 22,
|
|
username: 'deploy',
|
|
...overrides
|
|
}
|
|
}
|
|
|
|
function createResolved(overrides?: Partial<SystemSshResolvedConfig>): SystemSshResolvedConfig {
|
|
return {
|
|
hostname: '10.0.0.5',
|
|
port: 22,
|
|
user: 'deploy',
|
|
identityFile: ['/Users/me/.ssh/id_ed25519'],
|
|
forwardAgent: false,
|
|
identitiesOnly: true,
|
|
proxyUseFdpass: true,
|
|
controlMaster: 'no',
|
|
controlPersist: 'no',
|
|
...overrides
|
|
}
|
|
}
|
|
|
|
describe.skipIf(process.platform === 'win32')('getControlSocketPath', () => {
|
|
beforeEach(() => {
|
|
vi.unstubAllEnvs()
|
|
vi.stubEnv('XDG_RUNTIME_DIR', '')
|
|
tmpdirMock.mockReset()
|
|
tmpdirMock.mockReturnValue('/tmp')
|
|
mkdirSyncMock.mockReset()
|
|
lstatSyncMock.mockReset()
|
|
lstatSyncMock.mockReturnValue({
|
|
isDirectory: () => true,
|
|
uid: CURRENT_UID,
|
|
mode: 0o40700
|
|
})
|
|
})
|
|
|
|
it('returns a stable short private socket path for the same effective target', () => {
|
|
const first = getControlSocketPath(createTarget(), createResolved())
|
|
const second = getControlSocketPath(createTarget(), createResolved())
|
|
|
|
expect(first).toBe(second)
|
|
expect(first).toMatch(new RegExp(`/orca-ssh-${CURRENT_UID}/[0-9a-f]{16}$`))
|
|
// Why: OpenSSH creates a temporary mux listener by appending a suffix first.
|
|
expect(first!.length).toBeLessThanOrEqual(90)
|
|
expect(mkdirSyncMock).toHaveBeenCalledWith(`/tmp/orca-ssh-${CURRENT_UID}`, {
|
|
recursive: true,
|
|
mode: 0o700
|
|
})
|
|
})
|
|
|
|
it('changes the path when a config-backed target resolves to a different host', () => {
|
|
const before = getControlSocketPath(createTarget({ host: '10.0.0.5' }), createResolved())
|
|
const after = getControlSocketPath(
|
|
createTarget({ host: '10.0.0.9' }),
|
|
createResolved({ hostname: '10.0.0.9' })
|
|
)
|
|
|
|
expect(after).not.toBe(before)
|
|
})
|
|
|
|
it('changes the path when fresh ssh config resolution changes the route', () => {
|
|
const before = getControlSocketPath(
|
|
createTarget(),
|
|
createResolved({ proxyCommand: 'ssh -W %h:%p old-bastion' })
|
|
)
|
|
const after = getControlSocketPath(
|
|
createTarget(),
|
|
createResolved({ proxyCommand: 'ssh -W %h:%p new-bastion' })
|
|
)
|
|
|
|
expect(after).not.toBe(before)
|
|
})
|
|
|
|
it('uses XDG_RUNTIME_DIR before tmp when it is absolute and private', () => {
|
|
vi.stubEnv('XDG_RUNTIME_DIR', '/run/user/501')
|
|
|
|
const path = getControlSocketPath(createTarget(), createResolved())
|
|
|
|
expect(path).toMatch(/^\/run\/user\/501\/orca-ssh\/[0-9a-f]{16}$/)
|
|
})
|
|
|
|
it('falls back to non-multiplexed SSH when the control directory is unsafe', () => {
|
|
lstatSyncMock.mockReturnValue({
|
|
isDirectory: () => false,
|
|
uid: CURRENT_UID,
|
|
mode: 0o40700
|
|
})
|
|
|
|
expect(getControlSocketPath(createTarget(), createResolved())).toBeNull()
|
|
})
|
|
})
|