Improve SSH config target handling (#3899)

This commit is contained in:
Jinjing
2026-05-30 13:10:39 -07:00
committed by GitHub
parent 34504cbfd5
commit 28e2ce929f
8 changed files with 421 additions and 49 deletions
+47
View File
@@ -343,6 +343,33 @@ describe('buildConnectConfig', () => {
expect(config.username).toBe('admin')
})
it('uses ssh -G HostName when a config-host target still points at its alias', () => {
const config = buildConnectConfig(
makeTarget({ label: 'workbox', configHost: 'workbox', host: 'workbox' }),
makeResolved({ hostname: 'workbox.internal' })
)
expect(config.host).toBe('workbox.internal')
})
it('uses ssh -G Port when a config-host target still has the default port', () => {
const config = buildConnectConfig(
makeTarget({ configHost: 'workbox', host: 'workbox', port: 22 }),
makeResolved({ port: 2202 })
)
expect(config.port).toBe(2202)
})
it('keeps explicit non-default target ports ahead of ssh -G Port', () => {
const config = buildConnectConfig(
makeTarget({ configHost: 'workbox', host: 'workbox', port: 2022 }),
makeResolved({ port: 2202 })
)
expect(config.port).toBe(2022)
})
it('sets readyTimeout to CONNECT_TIMEOUT_MS', () => {
const config = buildConnectConfig(makeTarget(), null)
expect(config.readyTimeout).toBe(30_000)
@@ -358,6 +385,26 @@ describe('buildConnectConfig', () => {
expect(config.agent).toBe('/tmp/agent.sock')
})
it('enables agent forwarding when OpenSSH config requests it and an agent is available', () => {
const config = buildConnectConfig(makeTarget(), makeResolved({ forwardAgent: true }))
expect(config.agent).toBe('/tmp/agent.sock')
expect(config.agentForward).toBe(true)
})
it('does not enable agent forwarding without a usable agent', () => {
const platformSpy = vi.spyOn(process, 'platform', 'get').mockReturnValue('linux')
delete process.env.SSH_AUTH_SOCK
try {
const config = buildConnectConfig(makeTarget(), makeResolved({ forwardAgent: true }))
expect(config.agent).toBeUndefined()
expect(config.agentForward).toBeUndefined()
} finally {
platformSpy.mockRestore()
}
})
it('uses configured IdentityAgent before SSH_AUTH_SOCK', () => {
const config = buildConnectConfig(
makeTarget(),
+30 -2
View File
@@ -105,8 +105,8 @@ export function buildConnectConfig(
resolved: SshResolvedConfig | null,
options: BuildConnectConfigOptions = {}
): ConnectConfig {
const effectiveHost = target.host || resolved?.hostname || target.label
const effectivePort = target.port || resolved?.port || 22
const effectiveHost = resolveEffectiveHost(target, resolved)
const effectivePort = resolveEffectivePort(target, resolved)
const effectiveUser = target.username || resolved?.user || ''
const config: Record<string, unknown> = {
@@ -125,6 +125,10 @@ export function buildConnectConfig(
config.agent = agent
}
if (agent && resolved?.forwardAgent) {
config.agentForward = true
}
const key =
(options.includePrivateKey ?? !agent)
? resolvePrivateKey(target, resolved)
@@ -136,6 +140,30 @@ export function buildConnectConfig(
return config as ConnectConfig
}
function resolveEffectiveHost(target: SshTarget, resolved: SshResolvedConfig | null): string {
if (shouldUseResolvedEndpoint(target, resolved)) {
return resolved!.hostname
}
return target.host || resolved?.hostname || target.label
}
function resolveEffectivePort(target: SshTarget, resolved: SshResolvedConfig | null): number {
// Why: imported config aliases store 22 as the schema default even when an
// included/wildcard OpenSSH rule later resolves a different effective Port.
if (target.configHost && target.port === 22 && resolved?.port) {
return resolved.port
}
return target.port || resolved?.port || 22
}
function shouldUseResolvedEndpoint(target: SshTarget, resolved: SshResolvedConfig | null): boolean {
if (!target.configHost || !resolved?.hostname) {
return false
}
const host = target.host.trim()
return host === '' || host === target.configHost || host === target.label
}
// Why: ProxyJump and jumpHost are syntactic sugar for ProxyCommand.
// OpenSSH internally converts `ProxyJump bastion` to
// `ProxyCommand ssh -W %h:%p bastion`. We do the same so that ssh2