* Establish a chain of SSH connections through jump hosts. * @returns The final SSH client connected to the target host
(
jumpHosts: JumpHost[],
targetConfig: SSHTunnelConfig,
privateKey: Buffer | undefined
)
| 88 | * @returns The final SSH client connected to the target host |
| 89 | */ |
| 90 | private async establishChain( |
| 91 | jumpHosts: JumpHost[], |
| 92 | targetConfig: SSHTunnelConfig, |
| 93 | privateKey: Buffer | undefined |
| 94 | ): Promise<Client> { |
| 95 | let previousStream: Duplex | undefined; |
| 96 | |
| 97 | // Connect through each jump host |
| 98 | for (let i = 0; i < jumpHosts.length; i++) { |
| 99 | const jumpHost = jumpHosts[i]; |
| 100 | const nextHost = i + 1 < jumpHosts.length |
| 101 | ? jumpHosts[i + 1] |
| 102 | : { host: targetConfig.host, port: targetConfig.port || 22 }; |
| 103 | |
| 104 | // Per-hop credentials: use a hop's own resolved key when it has one, falling |
| 105 | // back to the target's key otherwise. The target password is always offered as |
| 106 | // a fallback (as before) — a hop may carry only a default-discovered key, so |
| 107 | // suppressing the password on "has a key" would break password auth. |
| 108 | const hopPrivateKey = jumpHost.privateKey ? this.loadPrivateKey(jumpHost.privateKey) : privateKey; |
| 109 | const hopPassword = targetConfig.password; |
| 110 | const hopPassphrase = jumpHost.passphrase ?? targetConfig.passphrase; |
| 111 | |
| 112 | let client: Client | null = null; |
| 113 | let forwardStream: Duplex; |
| 114 | try { |
| 115 | client = await this.connectToHost( |
| 116 | { |
| 117 | host: jumpHost.host, |
| 118 | port: jumpHost.port, |
| 119 | username: jumpHost.username || targetConfig.username, |
| 120 | }, |
| 121 | hopPassword, |
| 122 | hopPrivateKey, |
| 123 | hopPassphrase, |
| 124 | previousStream, |
| 125 | `jump host ${i + 1}`, |
| 126 | targetConfig.keepaliveInterval, |
| 127 | targetConfig.keepaliveCountMax |
| 128 | ); |
| 129 | |
| 130 | // Forward to the next host |
| 131 | console.error(` → Forwarding through ${jumpHost.host}:${jumpHost.port} to ${nextHost.host}:${nextHost.port}`); |
| 132 | forwardStream = await this.forwardTo(client, nextHost.host, nextHost.port); |
| 133 | } catch (error) { |
| 134 | if (client) { |
| 135 | try { |
| 136 | client.end(); |
| 137 | } catch { |
| 138 | // Ignore errors during cleanup of partially established client |
| 139 | } |
| 140 | } |
| 141 | throw error; |
| 142 | } |
| 143 | |
| 144 | this.sshClients.push(client); |
| 145 | previousStream = forwardStream; |
| 146 | } |
| 147 |
no test coverage detected