MCPcopy Create free account
hub / github.com/bytebase/dbhub / SSHTunnel

Class SSHTunnel

src/utils/ssh-tunnel.ts:12–376  ·  view source on GitHub ↗

Source from the content-addressed store, hash-verified

10 * Supports ProxyJump for multi-hop SSH connections through bastion/jump hosts.
11 */
12export class SSHTunnel {
13 private sshClients: Client[] = []; // All SSH clients in the chain
14 private localServer: Server | null = null;
15 private tunnelInfo: SSHTunnelInfo | null = null;
16 private isConnected: boolean = false;
17
18 /**
19 * Establish an SSH tunnel, optionally through jump hosts (ProxyJump).
20 * @param config SSH connection configuration
21 * @param options Tunnel options including target host and port
22 * @returns Promise resolving to tunnel information including local port
23 */
24 async establish(
25 config: SSHTunnelConfig,
26 options: SSHTunnelOptions
27 ): Promise<SSHTunnelInfo> {
28 if (this.isConnected) {
29 throw new Error('SSH tunnel is already established');
30 }
31
32 // Set isConnected immediately to prevent concurrent calls
33 this.isConnected = true;
34
35 try {
36 // Use the fully-resolved jump-host chain when available (per-hop config/auth
37 // from ~/.ssh/config); otherwise fall back to literal ProxyJump parsing.
38 const jumpHosts = config.resolvedJumpHosts
39 ?? (config.proxyJump ? parseJumpHosts(config.proxyJump) : []);
40
41 // Read the target's private key once.
42 const privateKeyBuffer = config.privateKey ? this.loadPrivateKey(config.privateKey) : undefined;
43
44 // Validate authentication
45 if (!config.password && !privateKeyBuffer) {
46 throw new Error('Either password or privateKey must be provided for SSH authentication');
47 }
48
49 // Establish the SSH connection chain
50 const finalClient = await this.establishChain(jumpHosts, config, privateKeyBuffer);
51
52 // Create local server for the tunnel
53 return await this.createLocalTunnel(finalClient, options);
54 } catch (error) {
55 this.cleanup();
56 throw error;
57 }
58 }
59
60 /**
61 * Load an SSH private key, supporting both a file path (with symlink resolution)
62 * and base64-encoded key content.
63 */
64 private loadPrivateKey(key: string): Buffer {
65 try {
66 const resolvedKeyPath = resolveSymlink(key);
67 return readFileSync(resolvedKeyPath);
68 } catch {
69 // Not a readable file — try base64 decode

Callers

nothing calls this directly

Calls

no outgoing calls

Tested by

no test coverage detected