import { Client, type ClientChannel } from "ssh2"; import type { Duplex } from "stream"; import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js"; import { tunnelLogger } from "../utils/logger.js"; export function getManagedTunnelAlgorithms() { return { kex: [ "curve25519-sha256", "curve25519-sha256@libssh.org", "ecdh-sha2-nistp521", "ecdh-sha2-nistp384", "ecdh-sha2-nistp256", "diffie-hellman-group-exchange-sha256", "diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1", "diffie-hellman-group-exchange-sha1", "diffie-hellman-group1-sha1", ], serverHostKey: [ "ssh-ed25519", "ecdsa-sha2-nistp521", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp256", "rsa-sha2-512", "rsa-sha2-256", "ssh-rsa", "ssh-dss", ], cipher: SSH_ALGORITHMS.cipher, hmac: [ "hmac-sha2-512-etm@openssh.com", "hmac-sha2-256-etm@openssh.com", "hmac-sha2-512", "hmac-sha2-256", "hmac-sha1", "hmac-md5", ], compress: ["none", "zlib@openssh.com", "zlib"], }; } export function applyAuthOptions( connOptions: Record, credentials: { password?: string; sshKey?: string; keyPassword?: string; keyType?: string; authMethod?: string; }, ): void { if (credentials.authMethod === "key" && credentials.sshKey) { const cleanKey = credentials.sshKey .trim() .replace(/\r\n/g, "\n") .replace(/\r/g, "\n"); connOptions.privateKey = Buffer.from(cleanKey, "utf8"); if (credentials.keyPassword) { connOptions.passphrase = credentials.keyPassword; } if (credentials.keyType && credentials.keyType !== "auto") { connOptions.privateKeyType = credentials.keyType; } } else { connOptions.password = credentials.password; } } export function connectClient( connOptions: Record, tunnelName: string, role: "source" | "endpoint", ): Promise { return new Promise((resolve, reject) => { const client = new Client(); let settled = false; client.once("ready", () => { settled = true; resolve(client); }); client.once("error", (error) => { if (!settled) { reject(error); return; } tunnelLogger.error("Managed tunnel SSH client error", error, { operation: "managed_tunnel_client_error", tunnelName, role, }); }); client.connect(connOptions); }); } export function forwardOut( client: Client, targetHost: string, targetPort: number, tunnelName?: string, ): Promise { return new Promise((resolve, reject) => { client.forwardOut("127.0.0.1", 0, targetHost, targetPort, (err, stream) => { if (err) { if (tunnelName) { tunnelLogger.error("Managed tunnel forwardOut failed", err, { operation: "managed_tunnel_forward_out_failed", tunnelName, targetHost, targetPort, }); } reject(err); return; } resolve(stream); }); }); } export function bindForwardIn( client: Client, bindHost: string, bindPort: number, ): Promise { return new Promise((resolve, reject) => { client.forwardIn(bindHost, bindPort, (err, actualPort) => { if (err) { reject(err); return; } resolve(actualPort || bindPort); }); }); } export function unbindForwardIn( client: Client, bindHost: string, bindPort: number, ): void { try { client.unforwardIn(bindHost, bindPort, (err) => { if (err) { tunnelLogger.warn("Failed to unbind managed tunnel listener", { operation: "managed_tunnel_unforward_failed", bindHost, bindPort, error: err.message, }); } }); } catch { // The connection may already be gone. } } export function pipeTunnelStreams( inbound: Duplex, outboundPromise: Promise, tunnelName: string, ): void { outboundPromise .then((outbound) => { inbound.pipe(outbound).pipe(inbound); inbound.on("error", () => outbound.destroy()); outbound.on("error", () => inbound.destroy()); }) .catch((error) => { tunnelLogger.error( "Failed to open managed tunnel outbound stream", error, { operation: "managed_tunnel_outbound_failed", tunnelName, }, ); inbound.destroy(); }); }