diff --git a/src/backend/hosts/docker/routes.ts b/src/backend/hosts/docker/routes.ts index 68e7d741..ae013bb5 100644 --- a/src/backend/hosts/docker/routes.ts +++ b/src/backend/hosts/docker/routes.ts @@ -994,7 +994,6 @@ export function registerDockerSshRoutes(app: express.Express): void { const jumpClient = await createJumpHostChain( host.jumpHosts as Array<{ hostId: number }>, userId, - proxyConfig, ); if (!jumpClient) { diff --git a/src/backend/hosts/file-manager/index.ts b/src/backend/hosts/file-manager/index.ts index 5f378a64..9a18822a 100644 --- a/src/backend/hosts/file-manager/index.ts +++ b/src/backend/hosts/file-manager/index.ts @@ -289,11 +289,7 @@ async function startDedicatedTransferConnect( const hasJumpHosts = jumpHosts && jumpHosts.length > 0; if (hasJumpHosts) { - const jumpClient = await createJumpHostChain( - jumpHosts, - userId, - proxyConfig, - ); + const jumpClient = await createJumpHostChain(jumpHosts, userId); if (!jumpClient) { throw new Error("Failed to connect through jump hosts for transfer"); } @@ -1616,11 +1612,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { `Connecting via ${resolvedJumpHosts.length} jump host(s)`, ), ); - const jumpClient = await createJumpHostChain( - resolvedJumpHosts, - userId, - proxyConfig, - ); + const jumpClient = await createJumpHostChain(resolvedJumpHosts, userId); if (!jumpClient) { fileLogger.error("Failed to establish jump host chain", { diff --git a/src/backend/hosts/guacamole/routes.ts b/src/backend/hosts/guacamole/routes.ts index 453da94d..71af3921 100644 --- a/src/backend/hosts/guacamole/routes.ts +++ b/src/backend/hosts/guacamole/routes.ts @@ -7,14 +7,13 @@ import { PermissionManager } from "../../utils/permission-manager.js"; import net from "net"; import crypto from "crypto"; import path from "path"; -import type { AuthenticatedRequest, ProxyNode } from "../../../types/index.js"; +import type { AuthenticatedRequest } from "../../../types/index.js"; import { createCurrentHostResolutionRepository, createCurrentSettingsRepository, } from "../../database/repositories/factory.js"; import { resolveGuacdOptions } from "../../utils/guacd-config.js"; import { createJumpHostChain } from "../jump-host-chain.js"; -import type { SOCKS5Config } from "../../utils/socks5-helper.js"; import { waitForGuacdOpen } from "./guacamole-server.js"; import { resolveJumpTunnelEndpoint } from "./jump-tunnel-endpoint.js"; @@ -499,40 +498,9 @@ router.post( perConnectionGuacdHost || resolveGuacdOptions(guacdUrl).host; const tunnelEndpoint = resolveJumpTunnelEndpoint(guacdHost); - let socks5ProxyChain: ProxyNode[] = []; - if (hostRecord.socks5ProxyChain) { - try { - socks5ProxyChain = - typeof hostRecord.socks5ProxyChain === "string" - ? JSON.parse(hostRecord.socks5ProxyChain as string) - : (hostRecord.socks5ProxyChain as ProxyNode[]); - } catch { - socks5ProxyChain = []; - } - } - - const proxyConfig: SOCKS5Config | null = - hostRecord.useSocks5 && - (hostRecord.socks5Host || socks5ProxyChain.length > 0) - ? { - useSocks5: hostRecord.useSocks5 as boolean, - socks5Host: hostRecord.socks5Host as string | undefined, - socks5Port: hostRecord.socks5Port as number | undefined, - socks5Username: hostRecord.socks5Username as - | string - | undefined, - socks5Password: hostRecord.socks5Password as - | string - | undefined, - socks5ProxyChain, - } - : null; - - const jumpClient = await createJumpHostChain( - jumpHosts, - userId, - proxyConfig, - ); + // The chain dials the first hop through that hop's own SOCKS5 + // settings; the target host's proxy config does not apply to it. + const jumpClient = await createJumpHostChain(jumpHosts, userId); if (!jumpClient) { guacLogger.error( diff --git a/src/backend/hosts/jump-host-chain.ts b/src/backend/hosts/jump-host-chain.ts index 5821fe76..87a401dc 100644 --- a/src/backend/hosts/jump-host-chain.ts +++ b/src/backend/hosts/jump-host-chain.ts @@ -1,10 +1,7 @@ import { Client as SSHClient } from "ssh2"; import { createCurrentHostResolutionRepository } from "../database/repositories/factory.js"; import { fileLogger } from "../utils/logger.js"; -import { - createSocks5Connection, - type SOCKS5Config, -} from "../utils/socks5-helper.js"; +import { createSocks5Connection } from "../utils/socks5-helper.js"; import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js"; import { SSHHostKeyVerifier } from "./host-key-verifier.js"; import { getJumpHostSocks5Config } from "./jump-host-proxy.js"; @@ -106,7 +103,6 @@ async function resolveJumpHost( export async function createJumpHostChain( jumpHosts: Array<{ hostId: number }>, userId: string, - socks5Config?: SOCKS5Config | null, ): Promise { if (!jumpHosts || jumpHosts.length === 0) { return null; @@ -138,10 +134,7 @@ export async function createJumpHostChain( } } - const firstHopSocks5Config = getJumpHostSocks5Config( - jumpHostConfigs[0], - socks5Config, - ); + const firstHopSocks5Config = getJumpHostSocks5Config(jumpHostConfigs[0]); let proxySocket: import("net").Socket | null = null; if (firstHopSocks5Config?.useSocks5) { const firstHop = jumpHostConfigs[0]!; diff --git a/src/backend/hosts/jump-host-proxy.test.ts b/src/backend/hosts/jump-host-proxy.test.ts new file mode 100644 index 00000000..877998e6 --- /dev/null +++ b/src/backend/hosts/jump-host-proxy.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; +import { getJumpHostSocks5Config } from "./jump-host-proxy.js"; + +describe("getJumpHostSocks5Config", () => { + it("uses the first jump host proxy settings", () => { + expect( + getJumpHostSocks5Config({ + useSocks5: true, + socks5Host: "proxy.internal", + socks5Port: 1080, + socks5Username: "user", + socks5Password: "secret", + }), + ).toEqual({ + useSocks5: true, + socks5Host: "proxy.internal", + socks5Port: 1080, + socks5Username: "user", + socks5Password: "secret", + socks5ProxyChain: [], + }); + }); + + it("does not use destination proxy settings for the first jump host", () => { + expect(getJumpHostSocks5Config({ useSocks5: false })).toBeNull(); + }); + + it("accepts a serialized proxy chain from the first jump host", () => { + const chain = [ + { + id: "proxy-1", + name: "Proxy 1", + host: "proxy.internal", + port: 1080, + type: "socks5" as const, + }, + ]; + + expect( + getJumpHostSocks5Config({ + useSocks5: true, + socks5ProxyChain: JSON.stringify(chain), + }), + ).toEqual({ + useSocks5: true, + socks5Host: undefined, + socks5Port: undefined, + socks5Username: undefined, + socks5Password: undefined, + socks5ProxyChain: chain, + }); + }); +}); diff --git a/src/backend/hosts/jump-host-proxy.ts b/src/backend/hosts/jump-host-proxy.ts index e16cb746..890446e0 100644 --- a/src/backend/hosts/jump-host-proxy.ts +++ b/src/backend/hosts/jump-host-proxy.ts @@ -29,15 +29,14 @@ function parseProxyChain(value: JumpHostProxyConfig["socks5ProxyChain"]) { export function getJumpHostSocks5Config( firstHop: JumpHostProxyConfig | null | undefined, - fallbackConfig?: SOCKS5Config | null, ): SOCKS5Config | null { if (!firstHop?.useSocks5) { - return fallbackConfig ?? null; + return null; } const socks5ProxyChain = parseProxyChain(firstHop.socks5ProxyChain); if (!firstHop.socks5Host && socks5ProxyChain.length === 0) { - return fallbackConfig ?? null; + return null; } return { diff --git a/src/backend/hosts/metrics/index.ts b/src/backend/hosts/metrics/index.ts index b4baddd3..628d8fde 100644 --- a/src/backend/hosts/metrics/index.ts +++ b/src/backend/hosts/metrics/index.ts @@ -464,24 +464,9 @@ class PollingManager { let isOnline: boolean; if (refreshedHost.jumpHosts && refreshedHost.jumpHosts.length > 0) { - const proxyConfig: SOCKS5Config | null = - refreshedHost.useSocks5 && - (refreshedHost.socks5Host || - (refreshedHost.socks5ProxyChain && - refreshedHost.socks5ProxyChain.length > 0)) - ? { - useSocks5: true, - socks5Host: refreshedHost.socks5Host, - socks5Port: refreshedHost.socks5Port, - socks5Username: refreshedHost.socks5Username, - socks5Password: refreshedHost.socks5Password, - socks5ProxyChain: refreshedHost.socks5ProxyChain, - } - : null; const jumpClient = await createJumpHostChain( refreshedHost.jumpHosts, userId, - proxyConfig, ); isOnline = jumpClient ? await tcpPingThroughJumpHost( @@ -1325,11 +1310,7 @@ function createSshFactory(host: SSHHostWithCredentials): () => Promise { let jumpClient: Client | null = null; if (hasJumpHosts) { - jumpClient = await createJumpHostChain( - host.jumpHosts!, - host.userId!, - proxyConfig, - ); + jumpClient = await createJumpHostChain(host.jumpHosts!, host.userId!); if (!jumpClient) { throw new Error("Failed to establish jump host chain"); diff --git a/src/backend/hosts/terminal/index.ts b/src/backend/hosts/terminal/index.ts index b575c749..c4b444cc 100644 --- a/src/backend/hosts/terminal/index.ts +++ b/src/backend/hosts/terminal/index.ts @@ -3000,7 +3000,6 @@ wss.on("connection", async (ws: WebSocket, req) => { const jumpClient = await createJumpHostChain( hostConfig.jumpHosts!, hostConfig.userId!, - proxyConfig, ); if (!jumpClient) { diff --git a/src/backend/hosts/tmux/index.ts b/src/backend/hosts/tmux/index.ts index c4446105..ec1016f0 100644 --- a/src/backend/hosts/tmux/index.ts +++ b/src/backend/hosts/tmux/index.ts @@ -145,11 +145,7 @@ export function connectToHost(host: SSHHost): () => Promise { let jumpClient: Client | null = null; if (host.jumpHosts && host.jumpHosts.length > 0 && host.userId) { - jumpClient = await createJumpHostChain( - host.jumpHosts, - host.userId, - proxyConfig, - ); + jumpClient = await createJumpHostChain(host.jumpHosts, host.userId); if (!jumpClient) { throw new Error("Failed to establish jump host chain"); }