diff --git a/docker/compose-dev.yml b/docker/compose-dev.yml index 14b703a8..3ed77957 100644 --- a/docker/compose-dev.yml +++ b/docker/compose-dev.yml @@ -13,6 +13,7 @@ services: PORT: "8080" NODE_ENV: development GUACD_HOST: "guacd-dev" + GUACD_TUNNEL_HOST: "termix-dev" GUACD_RECORDING_PATH: "/termix-data/session_recordings/guacamole" depends_on: - guacd-dev diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index eed4d172..856f827b 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -10,6 +10,7 @@ services: environment: PORT: "8080" GUACD_HOST: "guacd" + GUACD_TUNNEL_HOST: "termix" GUACD_RECORDING_PATH: "/termix-data/session_recordings/guacamole" depends_on: - guacd diff --git a/src/backend/hosts/guacamole/jump-tunnel-endpoint.ts b/src/backend/hosts/guacamole/jump-tunnel-endpoint.ts new file mode 100644 index 00000000..e05f1e29 --- /dev/null +++ b/src/backend/hosts/guacamole/jump-tunnel-endpoint.ts @@ -0,0 +1,15 @@ +const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]); + +export function resolveJumpTunnelEndpoint( + guacdHost: string, + tunnelHost = process.env.GUACD_TUNNEL_HOST, +): { bindHost: string; advertisedHost: string } { + if (LOOPBACK_HOSTS.has(guacdHost.toLowerCase())) { + return { bindHost: "127.0.0.1", advertisedHost: "127.0.0.1" }; + } + + return { + bindHost: "0.0.0.0", + advertisedHost: tunnelHost?.trim() || "termix", + }; +} diff --git a/src/backend/hosts/guacamole/routes.ts b/src/backend/hosts/guacamole/routes.ts index f5b07d5d..3a3882b7 100644 --- a/src/backend/hosts/guacamole/routes.ts +++ b/src/backend/hosts/guacamole/routes.ts @@ -15,6 +15,7 @@ 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"; const router = express.Router(); const tokenService = GuacamoleTokenService.getInstance(); @@ -485,6 +486,18 @@ router.post( if (jumpHosts.length > 0) { try { + let guacdUrl: string | undefined; + try { + guacdUrl = + (await createCurrentSettingsRepository().get("guac_url")) ?? + undefined; + } catch { + // Environment/default guacd configuration remains available. + } + const guacdHost = + perConnectionGuacdHost || resolveGuacdOptions(guacdUrl).host; + const tunnelEndpoint = resolveJumpTunnelEndpoint(guacdHost); + let socks5ProxyChain: ProxyNode[] = []; if (hostRecord.socks5ProxyChain) { try { @@ -550,7 +563,7 @@ router.post( ); }); server.on("error", reject); - server.listen(0, "127.0.0.1", () => { + server.listen(0, tunnelEndpoint.bindHost, () => { const addr = server.address() as net.AddressInfo; // Auto-cleanup after 1 hour setTimeout( @@ -563,7 +576,7 @@ router.post( resolve(addr.port); }); }); - hostname = "127.0.0.1"; + hostname = tunnelEndpoint.advertisedHost; port = tunnelPort; guacLogger.info("SSH tunnel established for guacamole", { operation: "guac_ssh_tunnel", diff --git a/src/backend/tests/hosts/guacamole/jump-tunnel-endpoint.test.ts b/src/backend/tests/hosts/guacamole/jump-tunnel-endpoint.test.ts new file mode 100644 index 00000000..cca9f1c9 --- /dev/null +++ b/src/backend/tests/hosts/guacamole/jump-tunnel-endpoint.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { resolveJumpTunnelEndpoint } from "../../../hosts/guacamole/jump-tunnel-endpoint.js"; + +describe("resolveJumpTunnelEndpoint", () => { + it("keeps an in-process guacd tunnel on loopback", () => { + expect(resolveJumpTunnelEndpoint("localhost")).toEqual({ + bindHost: "127.0.0.1", + advertisedHost: "127.0.0.1", + }); + }); + + it("exposes the tunnel to a separate guacd container", () => { + expect(resolveJumpTunnelEndpoint("guacd")).toEqual({ + bindHost: "0.0.0.0", + advertisedHost: "termix", + }); + }); + + it("supports a custom backend hostname for external guacd", () => { + expect( + resolveJumpTunnelEndpoint("guacd.example", "termix-backend"), + ).toEqual({ + bindHost: "0.0.0.0", + advertisedHost: "termix-backend", + }); + }); +});