Files
tsdocker/ts/classes.sshtunnel.ts

78 lines
2.4 KiB
TypeScript

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:<port>)
* is accessible as localhost:<port> 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 <localPort>:localhost:<localPort> [-i keyPath] user@host
*/
async openTunnel(builder: IRemoteBuilder, localPort: number): Promise<void> {
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<void> {
for (const builder of builders) {
await this.openTunnel(builder, localPort);
}
}
/**
* Closes all tunnel processes
*/
async closeAll(): Promise<void> {
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 = [];
}
}