import * as plugins from './tsdocker.plugins.js'; import { logger } from './tsdocker.logging.js'; import type { IRemoteBuilder } from './interfaces/index.js'; const smartshellInstance = new plugins.smartshell.Smartshell({ executor: 'bash', }); /** * Manages SSH reverse tunnels for remote builder nodes. * Opens tunnels so that the local staging registry (localhost:) * is accessible as localhost: on each remote machine. */ export class SshTunnelManager { private tunnelPids: number[] = []; /** * Opens a reverse SSH tunnel to make localPort accessible on the remote machine. * ssh -f -N -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes * -R :localhost: [-i keyPath] user@host */ async openTunnel(builder: IRemoteBuilder, localPort: number): Promise { const keyOpt = builder.sshKeyPath ? `-i ${builder.sshKeyPath} ` : ''; const cmd = [ 'ssh -f -N', '-o StrictHostKeyChecking=no', '-o ExitOnForwardFailure=yes', `-R ${localPort}:localhost:${localPort}`, `${keyOpt}${builder.host}`, ].join(' '); logger.log('info', `Opening SSH tunnel to ${builder.host} for port ${localPort}...`); const result = await smartshellInstance.exec(cmd); if (result.exitCode !== 0) { throw new Error( `Failed to open SSH tunnel to ${builder.host}: ${result.stderr || 'unknown error'}` ); } // Find the PID of the tunnel process we just started const pidResult = await smartshellInstance.exec( `pgrep -f "ssh.*-R ${localPort}:localhost:${localPort}.*${builder.host}" | tail -1` ); if (pidResult.exitCode === 0 && pidResult.stdout.trim()) { const pid = parseInt(pidResult.stdout.trim(), 10); if (!isNaN(pid)) { this.tunnelPids.push(pid); logger.log('ok', `SSH tunnel to ${builder.host} established (PID ${pid})`); } } } /** * Opens tunnels for all provided remote builders */ async openTunnels(builders: IRemoteBuilder[], localPort: number): Promise { for (const builder of builders) { await this.openTunnel(builder, localPort); } } /** * Closes all tunnel processes */ async closeAll(): Promise { for (const pid of this.tunnelPids) { try { process.kill(pid, 'SIGTERM'); logger.log('info', `Closed SSH tunnel (PID ${pid})`); } catch { // Process may have already exited } } this.tunnelPids = []; } }