2026-07-19 12:29:52 -05:00
import { type Response } from "express" ;
2026-06-04 15:16:53 -04:00
import {
createServer as createTcpServer ,
Socket as TcpSocket ,
type Server as TcpServer ,
} from "net" ;
2026-05-06 15:12:07 -05:00
import { Client , type ClientChannel } from "ssh2" ;
2026-07-19 12:29:52 -05:00
import { SSH_ALGORITHMS } from "../../utils/ssh-algorithms.js" ;
2025-09-12 14:42:00 -05:00
import { ChildProcess } from "child_process" ;
import axios from "axios" ;
2026-07-19 12:29:52 -05:00
import { createCurrentHostResolutionRepository } from "../../database/repositories/factory.js" ;
2025-09-12 14:42:00 -05:00
import type {
SSHHost ,
TunnelConfig ,
TunnelStatus ,
VerificationData ,
2026-07-19 12:29:52 -05:00
} from "../../../types/index.js" ;
import { CONNECTION_STATES } from "../../../types/index.js" ;
import { tunnelLogger } from "../../utils/logger.js" ;
import { logAudit } from "../../utils/audit-logger.js" ;
import { SystemCrypto } from "../../utils/system-crypto.js" ;
import { DataCrypto } from "../../utils/data-crypto.js" ;
import { createSocks5Connection } from "../../utils/socks5-helper.js" ;
import { withConnection } from "../ssh-connection-pool.js" ;
import { preparePrivateKeyForSSH2 } from "../../utils/ssh-key-utils.js" ;
2026-06-04 15:16:53 -04:00
import {
applyAuthOptions ,
bindForwardIn ,
connectClient ,
forwardOut ,
getManagedTunnelAlgorithms ,
pipeTunnelStreams ,
unbindForwardIn ,
2026-07-19 12:29:52 -05:00
} from "./ssh-primitives.js" ;
2026-06-04 15:16:53 -04:00
import {
classifyTunnelError ,
getTunnelBindHost ,
getTunnelMarker ,
getTunnelMode ,
getTunnelScope ,
normalizeTunnelName ,
2026-07-19 12:29:52 -05:00
} from "./utils.js" ;
2025-08-07 02:20:27 -05:00
2026-07-19 12:29:52 -05:00
import { resolveSshConnectConfigHost } from "../ssh-dns.js" ;
import { handleSocks5Connect } from "./socks5-relay.js" ;
2025-08-07 02:20:27 -05:00
2026-07-19 12:29:52 -05:00
export const activeTunnels = new Map < string , Client >();
export const retryCounters = new Map < string , number >();
export const connectionStatus = new Map < string , TunnelStatus >();
export const tunnelVerifications = new Map < string , VerificationData >();
export const manualDisconnects = new Set < string >();
export const verificationTimers = new Map < string , NodeJS.Timeout >();
export const activeRetryTimers = new Map < string , NodeJS.Timeout >();
export const countdownIntervals = new Map < string , NodeJS.Timeout >();
export const retryExhaustedTunnels = new Set < string >();
export const cleanupInProgress = new Set < string >();
export const tunnelConnecting = new Set < string >();
export const lastTunnelErrors = new Map < string , string >();
export const lastTunnelErrorTypes = new Map <
string ,
TunnelStatus [ "errorType" ]
> ();
2025-12-31 22:20:12 -06:00
2026-07-19 12:29:52 -05:00
export const tunnelConfigs = new Map < string , TunnelConfig >();
export const activeTunnelProcesses = new Map < string , ChildProcess >();
export const pendingTunnelOperations = new Map < string , Promise < void >>();
export const tunnelStatusClients = new Set < Response >();
2025-08-07 02:20:27 -05:00
2026-07-19 12:29:52 -05:00
export const INTERNAL_HOST_API_BASE_URL = "http://localhost:30001/host/db/host" ;
export const AUTOSTART_FETCH_RETRIES = 6 ;
2026-05-06 15:12:07 -05:00
2026-07-19 12:29:52 -05:00
export function sleep ( ms : number ) : Promise < void > {
2026-06-29 13:28:26 -05:00
return new Promise (( resolve ) => setTimeout ( resolve , ms ));
}
2026-07-19 12:29:52 -05:00
export function describeAxiosError ( error : unknown ) : string {
2026-06-29 13:28:26 -05:00
if ( axios . isAxiosError ( error )) {
return error . response
? ` ${ error . response . status } ${ error . response . statusText } `
: error . message ;
}
return error instanceof Error ? error . message : "Unknown error" ;
}
2026-07-19 12:29:52 -05:00
export async function fetchInternalHosts (
2026-06-29 13:28:26 -05:00
path : "internal" | "internal/all" ,
internalAuthToken : string ,
) : Promise < SSHHost [] > {
let lastError : unknown ;
for ( let attempt = 1 ; attempt <= AUTOSTART_FETCH_RETRIES ; attempt ++ ) {
try {
const response = await axios . get (
` ${ INTERNAL_HOST_API_BASE_URL } / ${ path } ` ,
{
headers : {
"Content-Type" : "application/json" ,
"X-Internal-Auth-Token" : internalAuthToken ,
},
timeout : 5000 ,
},
);
return response . data || [];
} catch ( error ) {
lastError = error ;
if ( attempt === AUTOSTART_FETCH_RETRIES ) {
break ;
}
const retryDelayMs = Math . min ( 500 * 2 ** ( attempt - 1 ), 5000 );
tunnelLogger . warn ( "Internal host API unavailable, retrying" , {
operation : "tunnel_autostart_fetch_retry" ,
path ,
attempt ,
maxAttempts : AUTOSTART_FETCH_RETRIES ,
retryDelayMs ,
error : describeAxiosError ( error ),
});
await sleep ( retryDelayMs );
}
}
throw new Error (
`Failed to fetch ${ path } hosts after ${ AUTOSTART_FETCH_RETRIES } attempts: ${ describeAxiosError ( lastError ) } ` ,
);
}
2026-07-19 12:29:52 -05:00
export type ActiveTunnelRuntime = {
2026-05-06 15:12:07 -05:00
sourceClient : Client ;
endpointClient? : Client ;
bindClient? : Client ;
bindHost? : string ;
bindPort? : number ;
2026-06-04 15:16:53 -04:00
tcpServer? : TcpServer ;
2026-05-06 15:12:07 -05:00
close : () => void ;
};
2026-07-19 12:29:52 -05:00
export const activeTunnelRuntimes = new Map < string , ActiveTunnelRuntime >();
2026-05-06 15:12:07 -05:00
2026-07-19 12:29:52 -05:00
export function findHostByTunnelEndpoint (
2026-06-29 13:28:26 -05:00
hosts : SSHHost [],
endpointHost? : string ,
) : SSHHost | undefined {
const value = endpointHost ? . trim ();
if ( ! value ) return undefined ;
return hosts . find (( host ) => {
const userAtIp = ` ${ host . username } @ ${ host . ip } ` ;
return (
String ( host . id ) === value ||
host . name === value ||
host . ip === value ||
userAtIp === value
);
});
}
2026-07-19 12:29:52 -05:00
export function broadcastTunnelStatus (
tunnelName : string ,
status : TunnelStatus ,
) : void {
2025-09-12 14:42:00 -05:00
if (
status . status === CONNECTION_STATES . CONNECTED &&
activeRetryTimers . has ( tunnelName )
) {
return ;
}
2025-08-07 02:20:27 -05:00
2026-05-06 15:12:07 -05:00
const nextStatus = { ... status };
2025-09-12 14:42:00 -05:00
if (
retryExhaustedTunnels . has ( tunnelName ) &&
2026-05-06 15:12:07 -05:00
nextStatus . status === CONNECTION_STATES . FAILED
2025-09-12 14:42:00 -05:00
) {
2026-05-06 15:12:07 -05:00
const previousReason = lastTunnelErrors . get ( tunnelName );
nextStatus . reason = previousReason
? `Max retries exhausted: ${ previousReason } `
: "Max retries exhausted" ;
2025-09-12 14:42:00 -05:00
}
2025-08-07 02:20:27 -05:00
2026-05-06 15:12:07 -05:00
if ( nextStatus . status === CONNECTION_STATES . FAILED && nextStatus . reason ) {
lastTunnelErrors . set ( tunnelName , nextStatus . reason );
if ( nextStatus . errorType ) {
lastTunnelErrorTypes . set ( tunnelName , nextStatus . errorType );
}
} else if (
( nextStatus . status === CONNECTION_STATES . CONNECTING ||
nextStatus . status === CONNECTION_STATES . RETRYING ||
nextStatus . status === CONNECTION_STATES . WAITING ) &&
! nextStatus . reason
) {
nextStatus . reason = lastTunnelErrors . get ( tunnelName );
nextStatus . errorType = lastTunnelErrorTypes . get ( tunnelName );
} else if (
nextStatus . status === CONNECTION_STATES . CONNECTED ||
( nextStatus . status === CONNECTION_STATES . DISCONNECTED &&
nextStatus . manualDisconnect )
) {
lastTunnelErrors . delete ( tunnelName );
lastTunnelErrorTypes . delete ( tunnelName );
}
connectionStatus . set ( tunnelName , nextStatus );
broadcastTunnelStatusSnapshot ();
2025-08-07 02:20:27 -05:00
}
2026-07-19 12:29:52 -05:00
export function getAllTunnelStatus () : Record < string , TunnelStatus > {
2025-09-12 14:42:00 -05:00
const tunnelStatus : Record < string , TunnelStatus > = {};
connectionStatus . forEach (( status , key ) => {
tunnelStatus [ key ] = status ;
});
return tunnelStatus ;
2025-08-07 02:20:27 -05:00
}
2026-07-19 12:29:52 -05:00
export function sendTunnelStatusSnapshot ( res : Response ) : void {
2026-05-06 15:12:07 -05:00
try {
res . write (
`event: statuses \ ndata: ${ JSON . stringify ( getAllTunnelStatus ()) } \ n \ n` ,
);
} catch {
tunnelStatusClients . delete ( res );
}
}
2026-07-19 12:29:52 -05:00
export function broadcastTunnelStatusSnapshot () : void {
2026-05-06 15:12:07 -05:00
for ( const client of tunnelStatusClients ) {
sendTunnelStatusSnapshot ( client );
}
}
2026-07-19 12:29:52 -05:00
export async function cleanupTunnelResources (
2025-10-01 15:40:10 -05:00
tunnelName : string ,
forceCleanup = false ,
2025-12-31 22:20:12 -06:00
) : Promise < void > {
2025-10-01 15:40:10 -05:00
if ( cleanupInProgress . has ( tunnelName )) {
return ;
}
if ( ! forceCleanup && tunnelConnecting . has ( tunnelName )) {
return ;
}
cleanupInProgress . add ( tunnelName );
2025-09-12 14:42:00 -05:00
const tunnelConfig = tunnelConfigs . get ( tunnelName );
2026-05-06 15:12:07 -05:00
const runtime = activeTunnelRuntimes . get ( tunnelName );
if ( runtime ) {
try {
runtime . close ();
} catch ( error ) {
tunnelLogger . error ( "Error while closing managed tunnel runtime" , error , {
operation : "managed_tunnel_cleanup" ,
tunnelName ,
});
}
activeTunnelRuntimes . delete ( tunnelName );
cleanupInProgress . delete ( tunnelName );
} else if ( tunnelConfig ) {
2025-12-31 22:20:12 -06:00
await new Promise < void >(( resolve ) => {
killRemoteTunnelByMarker ( tunnelConfig , tunnelName , ( err ) => {
cleanupInProgress . delete ( tunnelName );
if ( err ) {
tunnelLogger . error (
`Failed to kill remote tunnel for ' ${ tunnelName } ': ${ err . message } ` ,
);
}
resolve ();
});
2025-08-07 02:20:27 -05:00
});
2025-10-01 15:40:10 -05:00
} else {
cleanupInProgress . delete ( tunnelName );
2025-09-12 14:42:00 -05:00
}
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
if ( activeTunnelProcesses . has ( tunnelName )) {
try {
const proc = activeTunnelProcesses . get ( tunnelName );
if ( proc ) {
proc . kill ( "SIGTERM" );
}
} catch ( e ) {
tunnelLogger . error (
`Error while killing local ssh process for tunnel ' ${ tunnelName } '` ,
e ,
);
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
activeTunnelProcesses . delete ( tunnelName );
}
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
if ( activeTunnels . has ( tunnelName )) {
try {
const conn = activeTunnels . get ( tunnelName );
if ( conn ) {
conn . end ();
}
} catch ( e ) {
tunnelLogger . error (
`Error while closing SSH2 Client for tunnel ' ${ tunnelName } '` ,
e ,
);
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
activeTunnels . delete ( tunnelName );
}
if ( tunnelVerifications . has ( tunnelName )) {
const verification = tunnelVerifications . get ( tunnelName );
if ( verification ? . timeout ) clearTimeout ( verification . timeout );
try {
verification ? . conn . end ();
2026-02-12 22:28:13 -06:00
} catch ( error ) {
tunnelLogger . error ( "Error during tunnel cleanup" , error , {
operation : "tunnel_cleanup_error" ,
tunnelName ,
});
}
2025-09-12 14:42:00 -05:00
tunnelVerifications . delete ( tunnelName );
}
const timerKeys = [
tunnelName ,
` ${ tunnelName } _confirm` ,
` ${ tunnelName } _retry` ,
` ${ tunnelName } _verify_retry` ,
` ${ tunnelName } _ping` ,
];
timerKeys . forEach (( key ) => {
if ( verificationTimers . has ( key )) {
clearTimeout ( verificationTimers . get ( key ) ! );
verificationTimers . delete ( key );
}
});
if ( activeRetryTimers . has ( tunnelName )) {
clearTimeout ( activeRetryTimers . get ( tunnelName ) ! );
activeRetryTimers . delete ( tunnelName );
}
if ( countdownIntervals . has ( tunnelName )) {
clearInterval ( countdownIntervals . get ( tunnelName ) ! );
countdownIntervals . delete ( tunnelName );
}
2025-08-07 02:20:27 -05:00
}
2026-07-19 12:29:52 -05:00
export function resetRetryState ( tunnelName : string ) : void {
2025-09-12 14:42:00 -05:00
retryCounters . delete ( tunnelName );
retryExhaustedTunnels . delete ( tunnelName );
2026-05-06 15:12:07 -05:00
lastTunnelErrors . delete ( tunnelName );
lastTunnelErrorTypes . delete ( tunnelName );
2025-10-01 15:40:10 -05:00
cleanupInProgress . delete ( tunnelName );
tunnelConnecting . delete ( tunnelName );
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
if ( activeRetryTimers . has ( tunnelName )) {
clearTimeout ( activeRetryTimers . get ( tunnelName ) ! );
activeRetryTimers . delete ( tunnelName );
}
if ( countdownIntervals . has ( tunnelName )) {
clearInterval ( countdownIntervals . get ( tunnelName ) ! );
countdownIntervals . delete ( tunnelName );
}
[ "" , "_confirm" , "_retry" , "_verify_retry" , "_ping" ]. forEach (( suffix ) => {
const timerKey = ` ${ tunnelName }${ suffix } ` ;
if ( verificationTimers . has ( timerKey )) {
clearTimeout ( verificationTimers . get ( timerKey ) ! );
verificationTimers . delete ( timerKey );
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
});
2025-08-07 02:20:27 -05:00
}
2026-07-19 12:29:52 -05:00
export async function handleDisconnect (
2025-09-12 14:42:00 -05:00
tunnelName : string ,
tunnelConfig : TunnelConfig | null ,
shouldRetry = true ,
2025-12-31 22:20:12 -06:00
) : Promise < void > {
2025-09-12 14:42:00 -05:00
if ( tunnelVerifications . has ( tunnelName )) {
try {
const verification = tunnelVerifications . get ( tunnelName );
if ( verification ? . timeout ) clearTimeout ( verification . timeout );
verification ? . conn . end ();
2026-02-12 22:28:13 -06:00
} catch ( error ) {
tunnelLogger . error ( "Error during tunnel cleanup" , error , {
operation : "tunnel_cleanup_error" ,
tunnelName ,
});
}
2025-09-12 14:42:00 -05:00
tunnelVerifications . delete ( tunnelName );
}
2025-12-31 22:20:12 -06:00
while ( cleanupInProgress . has ( tunnelName )) {
await new Promise (( resolve ) => setTimeout ( resolve , 100 ));
}
await cleanupTunnelResources ( tunnelName );
2025-09-12 14:42:00 -05:00
if ( manualDisconnects . has ( tunnelName )) {
resetRetryState ( tunnelName );
broadcastTunnelStatus ( tunnelName , {
connected : false ,
status : CONNECTION_STATES.DISCONNECTED ,
manualDisconnect : true ,
});
return ;
}
if ( retryExhaustedTunnels . has ( tunnelName )) {
broadcastTunnelStatus ( tunnelName , {
connected : false ,
status : CONNECTION_STATES.FAILED ,
reason : "Max retries already exhausted" ,
});
return ;
}
if ( activeRetryTimers . has ( tunnelName )) {
return ;
}
if ( shouldRetry && tunnelConfig ) {
const maxRetries = tunnelConfig . maxRetries || 3 ;
const retryInterval = tunnelConfig . retryInterval || 5000 ;
let retryCount = retryCounters . get ( tunnelName ) || 0 ;
retryCount = retryCount + 1 ;
if ( retryCount > maxRetries ) {
tunnelLogger . error ( `All ${ maxRetries } retries failed for ${ tunnelName } ` );
retryExhaustedTunnels . add ( tunnelName );
activeTunnels . delete ( tunnelName );
retryCounters . delete ( tunnelName );
broadcastTunnelStatus ( tunnelName , {
connected : false ,
status : CONNECTION_STATES.FAILED ,
retryExhausted : true ,
reason : `Max retries exhausted` ,
});
return ;
}
retryCounters . set ( tunnelName , retryCount );
if ( retryCount <= maxRetries ) {
broadcastTunnelStatus ( tunnelName , {
connected : false ,
status : CONNECTION_STATES.RETRYING ,
retryCount : retryCount ,
maxRetries : maxRetries ,
nextRetryIn : retryInterval / 1000 ,
});
if ( activeRetryTimers . has ( tunnelName )) {
clearTimeout ( activeRetryTimers . get ( tunnelName ) ! );
activeRetryTimers . delete ( tunnelName );
}
const initialNextRetryIn = Math . ceil ( retryInterval / 1000 );
let currentNextRetryIn = initialNextRetryIn ;
broadcastTunnelStatus ( tunnelName , {
connected : false ,
status : CONNECTION_STATES.WAITING ,
retryCount : retryCount ,
maxRetries : maxRetries ,
nextRetryIn : currentNextRetryIn ,
});
const countdownInterval = setInterval (() => {
currentNextRetryIn -- ;
if ( currentNextRetryIn > 0 ) {
broadcastTunnelStatus ( tunnelName , {
connected : false ,
status : CONNECTION_STATES.WAITING ,
retryCount : retryCount ,
maxRetries : maxRetries ,
nextRetryIn : currentNextRetryIn ,
});
}
}, 1000 );
countdownIntervals . set ( tunnelName , countdownInterval );
const timer = setTimeout (() => {
clearInterval ( countdownInterval );
countdownIntervals . delete ( tunnelName );
activeRetryTimers . delete ( tunnelName );
if ( ! manualDisconnects . has ( tunnelName )) {
activeTunnels . delete ( tunnelName );
connectSSHTunnel ( tunnelConfig , retryCount ). catch (( error ) => {
tunnelLogger . error (
`Failed to connect tunnel ${ tunnelConfig . name } : ${ error instanceof Error ? error . message : "Unknown error" } ` ,
);
});
}
}, retryInterval );
activeRetryTimers . set ( tunnelName , timer );
}
} else {
broadcastTunnelStatus ( tunnelName , {
connected : false ,
status : CONNECTION_STATES.FAILED ,
});
activeTunnels . delete ( tunnelName );
}
}
2026-07-19 12:29:52 -05:00
export function setupPingInterval ( tunnelName : string ) : void {
2025-09-12 14:42:00 -05:00
const pingKey = ` ${ tunnelName } _ping` ;
if ( verificationTimers . has ( pingKey )) {
clearInterval ( verificationTimers . get ( pingKey ) ! );
verificationTimers . delete ( pingKey );
}
const pingInterval = setInterval (() => {
const currentStatus = connectionStatus . get ( tunnelName );
if ( currentStatus ? . status === CONNECTION_STATES . CONNECTED ) {
if ( ! activeTunnels . has ( tunnelName )) {
broadcastTunnelStatus ( tunnelName , {
connected : false ,
status : CONNECTION_STATES.DISCONNECTED ,
reason : "Tunnel connection lost" ,
});
clearInterval ( pingInterval );
verificationTimers . delete ( pingKey );
}
} else {
clearInterval ( pingInterval );
verificationTimers . delete ( pingKey );
}
}, 120000 );
verificationTimers . set ( pingKey , pingInterval );
}
2026-07-19 12:29:52 -05:00
export async function connectEndpointThroughSource (
2026-05-06 15:12:07 -05:00
sourceClient : Client ,
tunnelConfig : TunnelConfig ,
endpointCredentials : {
password? : string ;
sshKey? : string ;
keyPassword? : string ;
keyType? : string ;
authMethod? : string ;
},
) : Promise < Client > {
const endpointSock = await forwardOut (
sourceClient ,
tunnelConfig . endpointIP ,
tunnelConfig . endpointSSHPort ,
tunnelConfig . name ,
);
const endpointOptions : Record < string , unknown > = {
sock : endpointSock ,
username : tunnelConfig.endpointUsername ,
tryKeyboard : true ,
2026-06-04 15:16:53 -04:00
keepaliveInterval : tunnelConfig.keepaliveInterval ?? 30000 ,
keepaliveCountMax : tunnelConfig.keepaliveCountMax ?? 3 ,
2026-05-06 15:12:07 -05:00
readyTimeout : 60000 ,
tcpKeepAlive : true ,
tcpKeepAliveInitialDelay : 30000 ,
algorithms : getManagedTunnelAlgorithms (),
};
applyAuthOptions ( endpointOptions , endpointCredentials );
return connectClient ( endpointOptions , tunnelConfig . name , "endpoint" );
}
2026-07-19 12:29:52 -05:00
export function resolveS2SLocalTargetHost ( tunnelConfig : TunnelConfig ) : string {
2026-05-06 15:12:07 -05:00
const targetHost = tunnelConfig . targetHost ? . trim ();
if (
! targetHost ||
targetHost === tunnelConfig . endpointHost ||
targetHost === tunnelConfig . hostName
) {
return "127.0.0.1" ;
}
return targetHost ;
}
2026-07-19 12:29:52 -05:00
export function isSingleHostTunnel ( tunnelConfig : TunnelConfig ) : boolean {
2026-06-04 15:16:53 -04:00
if ( ! tunnelConfig . endpointHost && ! tunnelConfig . endpointIP ) return true ;
if (
tunnelConfig . endpointHost === "127.0.0.1" ||
tunnelConfig . endpointHost === "localhost"
) {
return true ;
}
if (
tunnelConfig . endpointIP &&
tunnelConfig . endpointIP === tunnelConfig . sourceIP &&
tunnelConfig . endpointSSHPort === tunnelConfig . sourceSSHPort
) {
return true ;
}
return false ;
}
2026-07-19 12:29:52 -05:00
export function shouldEstablishDirectTunnel (
tunnelConfig : TunnelConfig ,
) : boolean {
2026-06-29 13:28:26 -05:00
if ( isSingleHostTunnel ( tunnelConfig )) return true ;
const mode = getTunnelMode ( tunnelConfig );
return mode !== "remote" && ! tunnelConfig . endpointUsername ;
}
2026-07-19 12:29:52 -05:00
export async function establishDirectTunnel (
2026-06-04 15:16:53 -04:00
sourceClient : Client ,
tunnelConfig : TunnelConfig ,
) : Promise < void > {
const tunnelName = tunnelConfig . name ;
const mode = getTunnelMode ( tunnelConfig );
const bindHost = getTunnelBindHost ( tunnelConfig );
const sourcePort = tunnelConfig . sourcePort ;
2026-06-29 13:28:26 -05:00
const targetHost =
tunnelConfig . targetHost || tunnelConfig . endpointHost || "127.0.0.1" ;
2026-06-04 15:16:53 -04:00
const targetPort = tunnelConfig . endpointPort ;
if ( mode === "remote" ) {
const remoteBindPort = await bindForwardIn (
sourceClient ,
targetHost ,
sourcePort ,
);
const sockets = new Set < TcpSocket >();
sourceClient . on ( "tcp connection" , ( info , accept , reject ) => {
if ( info . destPort !== remoteBindPort ) {
reject ();
return ;
}
const inbound = accept ();
const local = new TcpSocket ();
sockets . add ( local );
local . connect ( targetPort , bindHost , () => {
pipeTunnelStreams ( inbound , Promise . resolve ( local ), tunnelName );
});
local . on ( "error" , () => {
inbound . destroy ();
sockets . delete ( local );
});
local . on ( "close" , () => sockets . delete ( local ));
});
const close = () => {
unbindForwardIn ( sourceClient , targetHost , remoteBindPort );
for ( const s of sockets ) s . destroy ();
sockets . clear ();
try {
sourceClient . end ();
} catch {
// expected
}
};
activeTunnelRuntimes . set ( tunnelName , {
sourceClient ,
bindHost : targetHost ,
bindPort : remoteBindPort ,
close ,
});
activeTunnels . set ( tunnelName , sourceClient );
return ;
}
// Local and dynamic modes: listen locally, forward through SSH
const sockets = new Set < TcpSocket >();
const tcpServer = createTcpServer (( socket ) => {
sockets . add ( socket );
socket . on ( "close" , () => sockets . delete ( socket ));
socket . on ( "error" , () => {
sockets . delete ( socket );
socket . destroy ();
});
if ( mode === "dynamic" ) {
handleSocks5Connect (
socket ,
( host , port ) => forwardOut ( sourceClient , host , port ),
tunnelName ,
);
return ;
}
forwardOut ( sourceClient , targetHost , targetPort , tunnelName )
. then (( outbound ) =>
pipeTunnelStreams ( socket , Promise . resolve ( outbound ), tunnelName ),
)
. catch (() => socket . destroy ());
});
await new Promise < void >(( resolve , reject ) => {
tcpServer . once ( "error" , reject );
tcpServer . listen ({ host : bindHost , port : sourcePort }, () => {
tcpServer . removeListener ( "error" , reject );
resolve ();
});
});
tunnelLogger . info ( "Direct tunnel listener started" , {
operation : "direct_tunnel_listen" ,
tunnelName ,
mode ,
bindHost ,
sourcePort ,
targetHost ,
targetPort ,
});
const close = () => {
for ( const s of sockets ) s . destroy ();
sockets . clear ();
tcpServer . close ();
try {
sourceClient . end ();
} catch {
// expected
}
};
sourceClient . on ( "close" , () => {
close ();
});
activeTunnelRuntimes . set ( tunnelName , {
sourceClient ,
tcpServer ,
bindHost ,
bindPort : sourcePort ,
close ,
});
activeTunnels . set ( tunnelName , sourceClient );
}
2026-07-19 12:29:52 -05:00
export async function establishManagedS2STunnel (
2026-05-06 15:12:07 -05:00
sourceClient : Client ,
tunnelConfig : TunnelConfig ,
endpointCredentials : {
password? : string ;
sshKey? : string ;
keyPassword? : string ;
keyType? : string ;
authMethod? : string ;
},
) : Promise < void > {
const tunnelName = tunnelConfig . name ;
const mode = getTunnelMode ( tunnelConfig );
const bindHost = getTunnelBindHost ( tunnelConfig );
const endpointClient = await connectEndpointThroughSource (
sourceClient ,
tunnelConfig ,
endpointCredentials ,
);
const bindClient = mode === "remote" ? endpointClient : sourceClient ;
const outboundClient = mode === "remote" ? sourceClient : endpointClient ;
const bindPort =
mode === "remote" ? tunnelConfig.endpointPort : tunnelConfig.sourcePort ;
const staticTargetHost =
mode === "remote"
? tunnelConfig . targetHost || "127.0.0.1"
: resolveS2SLocalTargetHost ( tunnelConfig );
const staticTargetPort =
mode === "remote" ? tunnelConfig.sourcePort : tunnelConfig.endpointPort ;
tunnelLogger . info ( "Managed S2S tunnel route resolved" , {
operation : "managed_tunnel_route_resolved" ,
tunnelName ,
mode ,
bindHost ,
bindPort ,
targetHost : staticTargetHost ,
targetPort : staticTargetPort ,
endpointHost : tunnelConfig.endpointHost ,
endpointIP : tunnelConfig.endpointIP ,
});
const actualPort = await bindForwardIn ( bindClient , bindHost , bindPort );
const tcpHandler = (
info : {
destIP : string ;
destPort : number ;
srcIP : string ;
srcPort : number ;
},
accept : () => ClientChannel ,
reject : () => void ,
) => {
if ( info . destPort !== actualPort ) {
reject ();
return ;
}
const inbound = accept ();
if ( mode === "dynamic" ) {
handleSocks5Connect (
inbound ,
( host , port ) => forwardOut ( outboundClient , host , port ),
tunnelName ,
);
return ;
}
pipeTunnelStreams (
inbound ,
forwardOut (
outboundClient ,
staticTargetHost ,
staticTargetPort ,
tunnelName ,
),
tunnelName ,
);
};
bindClient . on ( "tcp connection" , tcpHandler );
const close = () => {
bindClient . off ( "tcp connection" , tcpHandler );
unbindForwardIn ( bindClient , bindHost , actualPort );
try {
endpointClient . end ();
} catch {
// expected during shutdown
}
try {
sourceClient . end ();
} catch {
// expected during shutdown
}
};
activeTunnelRuntimes . set ( tunnelName , {
sourceClient ,
endpointClient ,
bindClient ,
bindHost ,
bindPort : actualPort ,
close ,
});
activeTunnels . set ( tunnelName , sourceClient );
}
2026-07-19 12:29:52 -05:00
export async function connectSSHTunnel (
2025-09-12 14:42:00 -05:00
tunnelConfig : TunnelConfig ,
retryAttempt = 0 ,
) : Promise < void > {
const tunnelName = tunnelConfig . name ;
2026-02-12 22:28:13 -06:00
tunnelLogger . info ( "Tunnel creation request received" , {
operation : "tunnel_create_request" ,
userId : tunnelConfig.sourceUserId ,
hostId : tunnelConfig.sourceHostId ,
tunnelName ,
tunnelType : tunnelConfig.tunnelType || "remote" ,
sourcePort : tunnelConfig.sourcePort ,
endpointHost : tunnelConfig.endpointHost ,
endpointPort : tunnelConfig.endpointPort ,
});
2025-09-12 14:42:00 -05:00
if ( manualDisconnects . has ( tunnelName )) {
return ;
}
2025-10-01 15:40:10 -05:00
tunnelConnecting . add ( tunnelName );
2026-04-22 16:55:23 -05:00
await cleanupTunnelResources ( tunnelName , true );
2025-09-12 14:42:00 -05:00
if ( retryAttempt === 0 ) {
retryExhaustedTunnels . delete ( tunnelName );
retryCounters . delete ( tunnelName );
}
const currentStatus = connectionStatus . get ( tunnelName );
if ( ! currentStatus || currentStatus . status !== CONNECTION_STATES . WAITING ) {
broadcastTunnelStatus ( tunnelName , {
connected : false ,
status : CONNECTION_STATES.CONNECTING ,
retryCount : retryAttempt > 0 ? retryAttempt : undefined ,
});
}
if (
! tunnelConfig ||
! tunnelConfig . sourceIP ||
! tunnelConfig . sourceUsername ||
! tunnelConfig . sourceSSHPort
) {
2026-01-24 19:49:42 -06:00
const missingFields = [];
if ( ! tunnelConfig ) missingFields . push ( "tunnelConfig" );
if ( ! tunnelConfig ? . sourceIP ) missingFields . push ( "sourceIP" );
if ( ! tunnelConfig ? . sourceUsername ) missingFields . push ( "sourceUsername" );
if ( ! tunnelConfig ? . sourceSSHPort ) missingFields . push ( "sourceSSHPort" );
tunnelLogger . error ( "Invalid tunnel connection details" , undefined , {
operation : "tunnel_connect_validation_failed" ,
2025-09-12 14:42:00 -05:00
tunnelName ,
2026-01-24 19:49:42 -06:00
missingFields : missingFields.join ( ", " ),
2025-09-12 14:42:00 -05:00
hasSourceIP : !! tunnelConfig ? . sourceIP ,
hasSourceUsername : !! tunnelConfig ? . sourceUsername ,
hasSourceSSHPort : !! tunnelConfig ? . sourceSSHPort ,
});
broadcastTunnelStatus ( tunnelName , {
connected : false ,
status : CONNECTION_STATES.FAILED ,
reason : "Missing required connection details" ,
});
2026-01-24 19:49:42 -06:00
tunnelConnecting . delete ( tunnelName );
2025-09-12 14:42:00 -05:00
return ;
}
let resolvedSourceCredentials = {
password : tunnelConfig.sourcePassword ,
sshKey : tunnelConfig.sourceSSHKey ,
keyPassword : tunnelConfig.sourceKeyPassword ,
keyType : tunnelConfig.sourceKeyType ,
authMethod : tunnelConfig.sourceAuthMethod ,
};
2025-12-31 22:20:12 -06:00
const effectiveUserId =
tunnelConfig . requestingUserId || tunnelConfig . sourceUserId ;
2025-09-12 14:42:00 -05:00
2026-04-22 16:55:23 -05:00
// Resolve source credentials server-side when not provided by frontend
if (
tunnelConfig . sourceHostId &&
effectiveUserId &&
! tunnelConfig . sourcePassword &&
! tunnelConfig . sourceSSHKey
) {
2025-12-31 22:20:12 -06:00
try {
2026-07-19 12:29:52 -05:00
const { resolveHostById } = await import ( "../host-resolver.js" );
2026-04-22 16:55:23 -05:00
const resolvedHost = await resolveHostById (
tunnelConfig . sourceHostId ,
effectiveUserId ,
);
if ( resolvedHost ) {
resolvedSourceCredentials = {
password : resolvedHost.password ,
sshKey : resolvedHost.key ,
keyPassword : resolvedHost.keyPassword ,
keyType : resolvedHost.keyType ,
authMethod : resolvedHost.authType ,
};
2026-05-28 22:05:25 -04:00
if ( tunnelConfig . keepaliveInterval === undefined ) {
tunnelConfig . keepaliveInterval =
typeof resolvedHost . terminalConfig ? . keepaliveInterval === "number"
? resolvedHost . terminalConfig . keepaliveInterval * 1000
: 60000 ;
}
if ( tunnelConfig . keepaliveCountMax === undefined ) {
tunnelConfig . keepaliveCountMax =
typeof resolvedHost . terminalConfig ? . keepaliveCountMax === "number"
? resolvedHost.terminalConfig.keepaliveCountMax
: 5 ;
}
2026-04-22 16:55:23 -05:00
}
} catch ( error ) {
tunnelLogger . warn ( "Failed to resolve source host credentials" , {
operation : "tunnel_connect" ,
tunnelName ,
sourceHostId : tunnelConfig.sourceHostId ,
error : error instanceof Error ? error . message : "Unknown error" ,
});
}
} else if ( tunnelConfig . sourceCredentialId && effectiveUserId ) {
// Legacy: credential resolution from credentialId
try {
if ( tunnelConfig . sourceHostId ) {
2026-07-19 12:29:52 -05:00
const { resolveHostById } = await import ( "../host-resolver.js" );
2026-04-22 16:55:23 -05:00
const resolvedHost = await resolveHostById (
tunnelConfig . sourceHostId ,
effectiveUserId ,
);
if ( resolvedHost ) {
resolvedSourceCredentials = {
password : resolvedHost.password ,
sshKey : resolvedHost.key ,
keyPassword : resolvedHost.keyPassword ,
keyType : resolvedHost.keyType ,
authMethod : resolvedHost.authType ,
};
2025-10-01 15:40:10 -05:00
}
2025-09-12 14:42:00 -05:00
}
} catch ( error ) {
2025-12-31 22:20:12 -06:00
tunnelLogger . warn ( "Failed to resolve source credentials" , {
2025-09-12 14:42:00 -05:00
operation : "tunnel_connect" ,
tunnelName ,
credentialId : tunnelConfig.sourceCredentialId ,
error : error instanceof Error ? error . message : "Unknown error" ,
});
}
}
let resolvedEndpointCredentials = {
password : tunnelConfig.endpointPassword ,
sshKey : tunnelConfig.endpointSSHKey ,
keyPassword : tunnelConfig.endpointKeyPassword ,
keyType : tunnelConfig.endpointKeyType ,
authMethod : tunnelConfig.endpointAuthMethod ,
};
if ( tunnelConfig . endpointCredentialId && tunnelConfig . endpointUserId ) {
try {
2026-07-19 12:29:52 -05:00
if ( DataCrypto . getUserDataKey ( tunnelConfig . endpointUserId ) !== null ) {
const credential =
await createCurrentHostResolutionRepository (). findCredentialByIdForUser (
tunnelConfig . endpointCredentialId ,
tunnelConfig . endpointUserId ,
);
2025-09-12 14:42:00 -05:00
2026-07-19 12:29:52 -05:00
if ( credential ) {
2025-10-01 15:40:10 -05:00
resolvedEndpointCredentials = {
2025-11-05 10:36:16 -06:00
password : credential.password as string | undefined ,
2026-05-28 22:29:20 -05:00
sshKey : ( credential . key || credential . privateKey ) as
| string
| undefined ,
2026-03-08 18:02:14 -05:00
keyPassword : credential.keyPassword as string | undefined ,
keyType : credential.keyType as string | undefined ,
authMethod : credential.authType as string ,
2025-10-01 15:40:10 -05:00
};
} else {
tunnelLogger . warn ( "No endpoint credentials found in database" , {
operation : "tunnel_connect" ,
tunnelName ,
credentialId : tunnelConfig.endpointCredentialId ,
});
}
2025-09-12 14:42:00 -05:00
}
} catch ( error ) {
tunnelLogger . warn (
`Failed to resolve endpoint credentials for tunnel ${ tunnelName } : ${ error instanceof Error ? error . message : "Unknown error" } ` ,
);
}
} else if ( tunnelConfig . endpointCredentialId ) {
tunnelLogger . warn ( "Missing userId for endpoint credential resolution" , {
operation : "tunnel_connect" ,
tunnelName ,
credentialId : tunnelConfig.endpointCredentialId ,
hasUserId : !! tunnelConfig . endpointUserId ,
});
}
2026-05-06 15:12:07 -05:00
if (
2026-06-29 13:28:26 -05:00
! shouldEstablishDirectTunnel ( tunnelConfig ) &&
2026-05-06 15:12:07 -05:00
resolvedEndpointCredentials . authMethod === "password" &&
! resolvedEndpointCredentials . password
) {
const errorMessage = `Cannot connect tunnel ' ${ tunnelName } ': endpoint host requires password authentication but no plaintext password available. Enable autostart for endpoint host or configure credentials in tunnel connection.` ;
tunnelLogger . error ( errorMessage , undefined , {
operation : "tunnel_endpoint_password_unavailable" ,
tunnelName ,
endpointHost : ` ${ tunnelConfig . endpointUsername } @ ${ tunnelConfig . endpointIP } : ${ tunnelConfig . endpointPort } ` ,
endpointAuthMethod : resolvedEndpointCredentials.authMethod ,
});
broadcastTunnelStatus ( tunnelName , {
connected : false ,
status : CONNECTION_STATES.FAILED ,
reason : errorMessage ,
});
tunnelConnecting . delete ( tunnelName );
return ;
}
if (
2026-06-29 13:28:26 -05:00
! shouldEstablishDirectTunnel ( tunnelConfig ) &&
2026-05-06 15:12:07 -05:00
resolvedEndpointCredentials . authMethod === "key" &&
! resolvedEndpointCredentials . sshKey
) {
const errorMessage = `Cannot connect tunnel ' ${ tunnelName } ': endpoint host requires key authentication but no plaintext key available. Enable autostart for endpoint host or configure credentials in tunnel connection.` ;
tunnelLogger . error ( errorMessage , undefined , {
operation : "tunnel_endpoint_key_unavailable" ,
tunnelName ,
endpointHost : ` ${ tunnelConfig . endpointUsername } @ ${ tunnelConfig . endpointIP } : ${ tunnelConfig . endpointPort } ` ,
endpointAuthMethod : resolvedEndpointCredentials.authMethod ,
});
broadcastTunnelStatus ( tunnelName , {
connected : false ,
status : CONNECTION_STATES.FAILED ,
reason : errorMessage ,
});
tunnelConnecting . delete ( tunnelName );
return ;
}
2025-09-12 14:42:00 -05:00
const conn = new Client ();
const connectionTimeout = setTimeout (() => {
if ( conn ) {
if ( activeRetryTimers . has ( tunnelName )) {
return ;
}
2026-01-24 19:49:42 -06:00
tunnelLogger . error (
`Tunnel connection timeout after 60 seconds for ' ${ tunnelName } '` ,
undefined ,
{
operation : "tunnel_connection_timeout" ,
tunnelName ,
sourceHost : ` ${ tunnelConfig . sourceUsername } @ ${ tunnelConfig . sourceIP } : ${ tunnelConfig . sourceSSHPort } ` ,
endpointHost : ` ${ tunnelConfig . endpointUsername } @ ${ tunnelConfig . endpointIP } : ${ tunnelConfig . endpointPort } ` ,
retryAttempt ,
usingSocks5 : tunnelConfig.useSocks5 || false ,
},
);
2025-09-12 14:42:00 -05:00
try {
conn . end ();
2026-03-08 18:02:14 -05:00
} catch {
// expected
}
2025-09-12 14:42:00 -05:00
activeTunnels . delete ( tunnelName );
if ( ! activeRetryTimers . has ( tunnelName )) {
handleDisconnect (
tunnelName ,
tunnelConfig ,
! manualDisconnects . has ( tunnelName ),
);
}
}
}, 60000 );
conn . on ( "error" , ( err ) => {
clearTimeout ( connectionTimeout );
2026-01-24 19:49:42 -06:00
2026-06-04 15:16:53 -04:00
const errorType = classifyTunnelError ( err . message );
2026-01-24 19:49:42 -06:00
tunnelLogger . error ( `Tunnel connection failed for ' ${ tunnelName } '` , err , {
operation : "tunnel_connect_error" ,
tunnelName ,
errorType ,
errorMessage : err.message ,
sourceHost : ` ${ tunnelConfig . sourceUsername } @ ${ tunnelConfig . sourceIP } : ${ tunnelConfig . sourceSSHPort } ` ,
endpointHost : ` ${ tunnelConfig . endpointUsername } @ ${ tunnelConfig . endpointIP } : ${ tunnelConfig . endpointPort } ` ,
tunnelType : tunnelConfig.tunnelType || "remote" ,
sourcePort : tunnelConfig.sourcePort ,
retryAttempt ,
usingSocks5 : tunnelConfig.useSocks5 || false ,
authMethod : tunnelConfig.sourceAuthMethod ,
});
2025-09-12 14:42:00 -05:00
2025-10-01 15:40:10 -05:00
tunnelConnecting . delete ( tunnelName );
2025-09-12 14:42:00 -05:00
if ( activeRetryTimers . has ( tunnelName )) {
return ;
}
if ( ! manualDisconnects . has ( tunnelName )) {
broadcastTunnelStatus ( tunnelName , {
connected : false ,
status : CONNECTION_STATES.FAILED ,
errorType : errorType ,
reason : err.message ,
});
}
activeTunnels . delete ( tunnelName );
const shouldNotRetry =
errorType === "AUTHENTICATION_FAILED" ||
errorType === "CONNECTION_FAILED" ||
manualDisconnects . has ( tunnelName );
handleDisconnect ( tunnelName , tunnelConfig , ! shouldNotRetry );
});
conn . on ( "close" , () => {
clearTimeout ( connectionTimeout );
2025-10-01 15:40:10 -05:00
tunnelConnecting . delete ( tunnelName );
2025-09-12 14:42:00 -05:00
if ( activeRetryTimers . has ( tunnelName )) {
return ;
}
if ( ! manualDisconnects . has ( tunnelName )) {
const currentStatus = connectionStatus . get ( tunnelName );
if ( ! currentStatus || currentStatus . status !== CONNECTION_STATES . FAILED ) {
broadcastTunnelStatus ( tunnelName , {
connected : false ,
status : CONNECTION_STATES.DISCONNECTED ,
});
}
if ( ! activeRetryTimers . has ( tunnelName )) {
handleDisconnect (
tunnelName ,
tunnelConfig ,
! manualDisconnects . has ( tunnelName ),
);
}
}
});
2026-05-06 15:12:07 -05:00
conn . on ( "ready" , async () => {
2025-09-12 14:42:00 -05:00
clearTimeout ( connectionTimeout );
2026-05-06 15:12:07 -05:00
tunnelLogger . info ( "Creating managed SSH tunnel" , {
operation : "managed_tunnel_connection_create" ,
2026-02-12 22:28:13 -06:00
userId : tunnelConfig.sourceUserId ,
hostId : tunnelConfig.sourceHostId ,
tunnelName ,
2026-05-06 15:12:07 -05:00
scope : getTunnelScope ( tunnelConfig ),
mode : getTunnelMode ( tunnelConfig ),
2026-02-12 22:28:13 -06:00
});
2025-09-12 14:42:00 -05:00
const isAlreadyVerifying = tunnelVerifications . has ( tunnelName );
if ( isAlreadyVerifying ) {
return ;
}
2026-05-06 15:12:07 -05:00
try {
if ( getTunnelScope ( tunnelConfig ) !== "s2s" ) {
throw new Error (
"C2S tunnels must be started from the desktop client local configuration" ,
2025-09-12 14:42:00 -05:00
);
}
2026-06-29 13:28:26 -05:00
if ( shouldEstablishDirectTunnel ( tunnelConfig )) {
2026-06-04 15:16:53 -04:00
await establishDirectTunnel ( conn , tunnelConfig );
} else {
await establishManagedS2STunnel (
conn ,
tunnelConfig ,
resolvedEndpointCredentials ,
);
}
2026-05-06 15:12:07 -05:00
tunnelConnecting . delete ( tunnelName );
tunnelLogger . success ( "Managed tunnel creation complete" , {
operation : "managed_tunnel_create_complete" ,
2026-02-12 22:28:13 -06:00
userId : tunnelConfig.sourceUserId ,
hostId : tunnelConfig.sourceHostId ,
tunnelName ,
2026-05-06 15:12:07 -05:00
mode : getTunnelMode ( tunnelConfig ),
2026-02-12 22:28:13 -06:00
sourcePort : tunnelConfig.sourcePort ,
endpointPort : tunnelConfig.endpointPort ,
});
2025-09-12 14:42:00 -05:00
2026-06-16 15:59:53 -05:00
logAudit ({
userId : tunnelConfig.sourceUserId ,
username : tunnelConfig.sourceUserId ,
action : "tunnel_connect" ,
resourceType : "tunnel" ,
resourceId : String ( tunnelConfig . sourceHostId ),
resourceName : tunnelName ,
details : JSON.stringify ({
mode : getTunnelMode ( tunnelConfig ),
sourcePort : tunnelConfig.sourcePort ,
}),
success : true ,
});
2026-05-06 15:12:07 -05:00
broadcastTunnelStatus ( tunnelName , {
connected : true ,
status : CONNECTION_STATES.CONNECTED ,
2025-09-12 14:42:00 -05:00
});
2026-05-06 15:12:07 -05:00
setupPingInterval ( tunnelName );
} catch ( error ) {
const message =
error instanceof Error ? error . message : "Failed to create tunnel" ;
2026-06-04 15:16:53 -04:00
const errorType = classifyTunnelError ( message );
2026-05-06 15:12:07 -05:00
tunnelLogger . error ( "Failed to create managed tunnel" , error , {
operation : "managed_tunnel_create_failed" ,
tunnelName ,
errorType ,
retryAttempt ,
2025-09-12 14:42:00 -05:00
});
2026-05-06 15:12:07 -05:00
tunnelConnecting . delete ( tunnelName );
activeTunnels . delete ( tunnelName );
activeTunnelRuntimes . delete ( tunnelName );
try {
conn . end ();
} catch {
// expected
}
broadcastTunnelStatus ( tunnelName , {
connected : false ,
status : CONNECTION_STATES.FAILED ,
errorType ,
reason : message ,
});
const shouldNotRetry =
errorType === "AUTHENTICATION_FAILED" ||
errorType === "CONNECTION_FAILED" ;
handleDisconnect ( tunnelName , tunnelConfig , ! shouldNotRetry );
}
2025-09-12 14:42:00 -05:00
});
2025-11-05 10:36:16 -06:00
const connOptions : Record < string , unknown > = {
2026-03-08 18:02:14 -05:00
host :
tunnelConfig.sourceIP?.replace ( /^\[|\]$/g , "" ) || tunnelConfig . sourceIP ,
2025-09-12 14:42:00 -05:00
port : tunnelConfig.sourceSSHPort ,
username : tunnelConfig.sourceUsername ,
2025-11-05 10:36:16 -06:00
tryKeyboard : true ,
2026-06-04 15:16:53 -04:00
keepaliveInterval : tunnelConfig.keepaliveInterval ?? 30000 ,
keepaliveCountMax : tunnelConfig.keepaliveCountMax ?? 3 ,
2025-09-12 14:42:00 -05:00
readyTimeout : 60000 ,
tcpKeepAlive : true ,
2025-11-05 10:36:16 -06:00
tcpKeepAliveInitialDelay : 30000 ,
env : {
TERM : "xterm-256color" ,
LANG : "en_US.UTF-8" ,
LC_ALL : "en_US.UTF-8" ,
LC_CTYPE : "en_US.UTF-8" ,
LC_MESSAGES : "en_US.UTF-8" ,
LC_MONETARY : "en_US.UTF-8" ,
LC_NUMERIC : "en_US.UTF-8" ,
LC_TIME : "en_US.UTF-8" ,
LC_COLLATE : "en_US.UTF-8" ,
COLORTERM : "truecolor" ,
},
2025-09-12 14:42:00 -05:00
algorithms : {
kex : [
2025-11-05 10:36:16 -06:00
"curve25519-sha256" ,
"curve25519-sha256@libssh.org" ,
"ecdh-sha2-nistp521" ,
"ecdh-sha2-nistp384" ,
"ecdh-sha2-nistp256" ,
"diffie-hellman-group-exchange-sha256" ,
2025-09-12 14:42:00 -05:00
"diffie-hellman-group14-sha256" ,
"diffie-hellman-group14-sha1" ,
"diffie-hellman-group-exchange-sha1" ,
2025-11-05 10:36:16 -06:00
"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" ,
2025-09-12 14:42:00 -05:00
],
2026-04-22 16:55:23 -05:00
cipher : SSH_ALGORITHMS.cipher ,
2025-10-01 15:40:10 -05:00
hmac : [
"hmac-sha2-512-etm@openssh.com" ,
2025-11-05 10:36:16 -06:00
"hmac-sha2-256-etm@openssh.com" ,
2025-10-01 15:40:10 -05:00
"hmac-sha2-512" ,
2025-11-05 10:36:16 -06:00
"hmac-sha2-256" ,
2025-10-01 15:40:10 -05:00
"hmac-sha1" ,
"hmac-md5" ,
],
2025-09-12 14:42:00 -05:00
compress : [ "none" , "zlib@openssh.com" , "zlib" ],
},
};
if (
resolvedSourceCredentials . authMethod === "key" &&
resolvedSourceCredentials . sshKey
) {
2026-06-29 13:28:26 -05:00
try {
connOptions . privateKey = preparePrivateKeyForSSH2 (
resolvedSourceCredentials . sshKey ,
resolvedSourceCredentials . keyPassword ,
);
} catch ( error ) {
const message =
error instanceof Error ? error . message : "Invalid SSH key format" ;
2025-09-12 14:42:00 -05:00
tunnelLogger . error (
2026-06-29 13:28:26 -05:00
`Invalid SSH key format for tunnel ' ${ tunnelName } ': ${ message } ` ,
2026-01-24 19:49:42 -06:00
undefined ,
{
operation : "tunnel_invalid_ssh_key_format" ,
tunnelName ,
sourceHost : ` ${ tunnelConfig . sourceUsername } @ ${ tunnelConfig . sourceIP } : ${ tunnelConfig . sourceSSHPort } ` ,
keyType : resolvedSourceCredentials.keyType ,
},
2025-09-12 14:42:00 -05:00
);
broadcastTunnelStatus ( tunnelName , {
connected : false ,
status : CONNECTION_STATES.FAILED ,
2026-06-29 13:28:26 -05:00
reason : message ,
2025-09-12 14:42:00 -05:00
});
2026-01-24 19:49:42 -06:00
tunnelConnecting . delete ( tunnelName );
2025-09-12 14:42:00 -05:00
return ;
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
if ( resolvedSourceCredentials . keyPassword ) {
connOptions . passphrase = resolvedSourceCredentials . keyPassword ;
}
if (
resolvedSourceCredentials . keyType &&
resolvedSourceCredentials . keyType !== "auto"
) {
connOptions . privateKeyType = resolvedSourceCredentials . keyType ;
}
} else if ( resolvedSourceCredentials . authMethod === "key" ) {
tunnelLogger . error (
`SSH key authentication requested but no key provided for tunnel ' ${ tunnelName } '` ,
2026-01-24 19:49:42 -06:00
undefined ,
{
operation : "tunnel_ssh_key_missing" ,
tunnelName ,
sourceHost : ` ${ tunnelConfig . sourceUsername } @ ${ tunnelConfig . sourceIP } : ${ tunnelConfig . sourceSSHPort } ` ,
authMethod : resolvedSourceCredentials.authMethod ,
},
2025-09-12 14:42:00 -05:00
);
broadcastTunnelStatus ( tunnelName , {
connected : false ,
status : CONNECTION_STATES.FAILED ,
reason : "SSH key authentication requested but no key provided" ,
});
2026-01-24 19:49:42 -06:00
tunnelConnecting . delete ( tunnelName );
2025-09-12 14:42:00 -05:00
return ;
} else {
connOptions . password = resolvedSourceCredentials . password ;
}
const finalStatus = connectionStatus . get ( tunnelName );
if ( ! finalStatus || finalStatus . status !== CONNECTION_STATES . WAITING ) {
broadcastTunnelStatus ( tunnelName , {
connected : false ,
status : CONNECTION_STATES.CONNECTING ,
retryCount : retryAttempt > 0 ? retryAttempt : undefined ,
});
}
2025-12-31 22:20:12 -06:00
if (
tunnelConfig . useSocks5 &&
( tunnelConfig . socks5Host ||
( tunnelConfig . socks5ProxyChain &&
tunnelConfig . socks5ProxyChain . length > 0 ))
) {
try {
const socks5Socket = await createSocks5Connection (
tunnelConfig . sourceIP ,
tunnelConfig . sourceSSHPort ,
{
useSocks5 : tunnelConfig.useSocks5 ,
socks5Host : tunnelConfig.socks5Host ,
socks5Port : tunnelConfig.socks5Port ,
socks5Username : tunnelConfig.socks5Username ,
socks5Password : tunnelConfig.socks5Password ,
socks5ProxyChain : tunnelConfig.socks5ProxyChain ,
},
);
if ( socks5Socket ) {
connOptions . sock = socks5Socket ;
conn . connect ( connOptions );
return ;
}
} catch ( socks5Error ) {
tunnelLogger . error ( "SOCKS5 connection failed for tunnel" , socks5Error , {
2026-01-24 19:49:42 -06:00
operation : "tunnel_socks5_connection_failed" ,
2025-12-31 22:20:12 -06:00
tunnelName ,
2026-01-24 19:49:42 -06:00
sourceHost : ` ${ tunnelConfig . sourceIP } : ${ tunnelConfig . sourceSSHPort } ` ,
2025-12-31 22:20:12 -06:00
proxyHost : tunnelConfig.socks5Host ,
proxyPort : tunnelConfig.socks5Port || 1080 ,
2026-01-24 19:49:42 -06:00
hasProxyAuth : !! (
tunnelConfig . socks5Username && tunnelConfig . socks5Password
),
errorMessage :
socks5Error instanceof Error ? socks5Error . message : "Unknown error" ,
2025-12-31 22:20:12 -06:00
});
broadcastTunnelStatus ( tunnelName , {
connected : false ,
status : CONNECTION_STATES.FAILED ,
reason :
"SOCKS5 proxy connection failed: " +
( socks5Error instanceof Error
? socks5Error . message
: "Unknown error" ),
});
2026-01-24 19:49:42 -06:00
tunnelConnecting . delete ( tunnelName );
2025-12-31 22:20:12 -06:00
return ;
}
}
2026-07-19 12:29:52 -05:00
try {
await resolveSshConnectConfigHost ( connOptions );
} catch ( error ) {
tunnelLogger . error ( "Tunnel source hostname resolution failed" , error , {
operation : "tunnel_dns_resolve" ,
tunnelName ,
sourceHost : ` ${ tunnelConfig . sourceIP } : ${ tunnelConfig . sourceSSHPort } ` ,
retryAttempt ,
});
broadcastTunnelStatus ( tunnelName , {
connected : false ,
status : CONNECTION_STATES.FAILED ,
reason :
error instanceof Error
? error . message
: "Failed to resolve tunnel source hostname" ,
});
tunnelConnecting . delete ( tunnelName );
return ;
}
2025-09-12 14:42:00 -05:00
conn . connect ( connOptions );
2025-08-07 02:20:27 -05:00
}
2026-07-19 12:29:52 -05:00
export async function killRemoteTunnelByMarker (
2025-09-12 14:42:00 -05:00
tunnelConfig : TunnelConfig ,
tunnelName : string ,
callback : ( err? : Error ) => void ,
) {
const tunnelMarker = getTunnelMarker ( tunnelName );
2026-02-12 22:28:13 -06:00
tunnelLogger . info ( "Killing remote tunnel process" , {
operation : "tunnel_remote_kill" ,
userId : tunnelConfig.sourceUserId ,
hostId : tunnelConfig.sourceHostId ,
tunnelName ,
marker : tunnelMarker ,
});
2025-10-01 15:40:10 -05:00
let resolvedSourceCredentials = {
password : tunnelConfig.sourcePassword ,
sshKey : tunnelConfig.sourceSSHKey ,
keyPassword : tunnelConfig.sourceKeyPassword ,
keyType : tunnelConfig.sourceKeyType ,
authMethod : tunnelConfig.sourceAuthMethod ,
};
2026-04-22 16:55:23 -05:00
if (
tunnelConfig . sourceHostId &&
tunnelConfig . sourceUserId &&
! tunnelConfig . sourcePassword &&
! tunnelConfig . sourceSSHKey
) {
2025-10-01 15:40:10 -05:00
try {
2026-07-19 12:29:52 -05:00
const { resolveHostById } = await import ( "../host-resolver.js" );
2026-04-22 16:55:23 -05:00
const resolvedHost = await resolveHostById (
tunnelConfig . sourceHostId ,
tunnelConfig . sourceUserId ,
);
if ( resolvedHost ) {
resolvedSourceCredentials = {
password : resolvedHost.password ,
sshKey : resolvedHost.key ,
keyPassword : resolvedHost.keyPassword ,
keyType : resolvedHost.keyType ,
authMethod : resolvedHost.authType ,
};
2025-10-01 15:40:10 -05:00
}
} catch ( error ) {
tunnelLogger . warn ( "Failed to resolve source credentials for cleanup" , {
tunnelName ,
2026-04-22 16:55:23 -05:00
sourceHostId : tunnelConfig.sourceHostId ,
2025-10-01 15:40:10 -05:00
error : error instanceof Error ? error . message : "Unknown error" ,
});
}
}
if (
resolvedSourceCredentials . authMethod === "key" &&
resolvedSourceCredentials . sshKey
) {
2026-06-29 13:28:26 -05:00
try {
preparePrivateKeyForSSH2 (
resolvedSourceCredentials . sshKey ,
resolvedSourceCredentials . keyPassword ,
);
} catch ( error ) {
callback (
error instanceof Error ? error : new Error ( "Invalid SSH key format" ),
);
2025-09-12 14:42:00 -05:00
return ;
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
}
2025-10-01 15:40:10 -05:00
2026-03-08 18:02:14 -05:00
const poolKey = `tunnel: ${ tunnelConfig . sourceUserId } : ${ tunnelConfig . sourceIP } : ${ tunnelConfig . sourceSSHPort } : ${ tunnelConfig . sourceUsername } ` ;
2025-10-01 15:40:10 -05:00
2026-03-08 18:02:14 -05:00
const factory = async () : Promise < Client > => {
const connOptions : Record < string , unknown > = {
host :
tunnelConfig.sourceIP?.replace ( /^\[|\]$/g , "" ) || tunnelConfig . sourceIP ,
port : tunnelConfig.sourceSSHPort ,
username : tunnelConfig.sourceUsername ,
2026-05-28 22:05:25 -04:00
keepaliveInterval : tunnelConfig.keepaliveInterval ?? 60000 ,
keepaliveCountMax : tunnelConfig.keepaliveCountMax ?? 5 ,
2026-03-08 18:02:14 -05:00
readyTimeout : 60000 ,
tcpKeepAlive : true ,
2026-05-28 22:05:25 -04:00
tcpKeepAliveInitialDelay : 30000 ,
2026-03-08 18:02:14 -05:00
algorithms : {
kex : [
"diffie-hellman-group14-sha256" ,
"diffie-hellman-group14-sha1" ,
"diffie-hellman-group1-sha1" ,
"diffie-hellman-group-exchange-sha256" ,
"diffie-hellman-group-exchange-sha1" ,
"ecdh-sha2-nistp256" ,
"ecdh-sha2-nistp384" ,
"ecdh-sha2-nistp521" ,
],
cipher : [
"aes128-ctr" ,
"aes192-ctr" ,
"aes256-ctr" ,
"aes128-gcm@openssh.com" ,
"aes256-gcm@openssh.com" ,
"aes128-cbc" ,
"aes192-cbc" ,
"aes256-cbc" ,
"3des-cbc" ,
],
hmac : [
"hmac-sha2-256-etm@openssh.com" ,
"hmac-sha2-512-etm@openssh.com" ,
"hmac-sha2-256" ,
"hmac-sha2-512" ,
"hmac-sha1" ,
"hmac-md5" ,
],
compress : [ "none" , "zlib@openssh.com" , "zlib" ],
},
};
2025-10-01 15:40:10 -05:00
2026-03-08 18:02:14 -05:00
if (
resolvedSourceCredentials . authMethod === "key" &&
resolvedSourceCredentials . sshKey
) {
2026-06-29 13:28:26 -05:00
connOptions . privateKey = preparePrivateKeyForSSH2 (
resolvedSourceCredentials . sshKey ,
resolvedSourceCredentials . keyPassword ,
);
2026-03-08 18:02:14 -05:00
if ( resolvedSourceCredentials . keyPassword ) {
connOptions . passphrase = resolvedSourceCredentials . keyPassword ;
}
if (
resolvedSourceCredentials . keyType &&
resolvedSourceCredentials . keyType !== "auto"
) {
connOptions . privateKeyType = resolvedSourceCredentials . keyType ;
}
} else {
connOptions . password = resolvedSourceCredentials . password ;
}
2025-10-01 15:40:10 -05:00
2026-03-08 18:02:14 -05:00
if (
tunnelConfig . useSocks5 &&
( tunnelConfig . socks5Host ||
( tunnelConfig . socks5ProxyChain &&
tunnelConfig . socks5ProxyChain . length > 0 ))
) {
2025-12-31 22:20:12 -06:00
try {
const socks5Socket = await createSocks5Connection (
tunnelConfig . sourceIP ,
tunnelConfig . sourceSSHPort ,
{
useSocks5 : tunnelConfig.useSocks5 ,
socks5Host : tunnelConfig.socks5Host ,
socks5Port : tunnelConfig.socks5Port ,
socks5Username : tunnelConfig.socks5Username ,
socks5Password : tunnelConfig.socks5Password ,
socks5ProxyChain : tunnelConfig.socks5ProxyChain ,
},
);
if ( socks5Socket ) {
connOptions . sock = socks5Socket ;
} else {
2026-03-08 18:02:14 -05:00
throw new Error ( "Failed to create SOCKS5 connection" );
2025-12-31 22:20:12 -06:00
}
} catch ( socks5Error ) {
tunnelLogger . error (
"SOCKS5 connection failed for killing tunnel" ,
socks5Error ,
{
operation : "socks5_connect_kill" ,
tunnelName ,
proxyHost : tunnelConfig.socks5Host ,
proxyPort : tunnelConfig.socks5Port || 1080 ,
},
);
2026-03-08 18:02:14 -05:00
throw new Error (
"SOCKS5 proxy connection failed: " +
( socks5Error instanceof Error
? socks5Error . message
: "Unknown error" ),
2026-05-06 15:12:07 -05:00
{ cause : socks5Error },
2025-12-31 22:20:12 -06:00
);
}
2026-03-08 18:02:14 -05:00
}
2026-07-19 12:29:52 -05:00
if ( ! connOptions . sock ) {
await resolveSshConnectConfigHost ( connOptions );
}
2026-03-08 18:02:14 -05:00
return new Promise < Client >(( resolve , reject ) => {
const conn = new Client ();
conn . on ( "ready" , () => resolve ( conn ));
conn . on ( "error" , ( err ) => reject ( err ));
conn . connect ( connOptions );
});
};
const execCommand = ( client : Client , cmd : string ) : Promise < string > =>
new Promise (( resolve , reject ) => {
client . exec ( cmd , ( err , stream ) => {
if ( err ) {
reject ( err );
return ;
}
let output = "" ;
stream . on ( "data" , ( data : Buffer ) => {
output += data . toString ();
});
stream . stderr . on ( "data" , ( data : Buffer ) => {
const stderr = data . toString (). trim ();
if ( stderr && ! stderr . includes ( "debug1" )) {
tunnelLogger . warn (
`Kill command stderr for ' ${ tunnelName } ': ${ stderr } ` ,
);
}
});
stream . on ( "close" , () => resolve ( output . trim ()));
});
});
try {
await withConnection ( poolKey , factory , async ( client ) => {
2026-06-04 15:16:53 -04:00
const checkCmd = `ps aux | grep -F ' ${ tunnelMarker } ' | grep -v grep` ;
2026-03-08 18:02:14 -05:00
const checkOutput = await execCommand ( client , checkCmd );
if ( ! checkOutput ) {
tunnelLogger . warn ( "Remote tunnel process not found" , {
operation : "tunnel_remote_not_found" ,
userId : tunnelConfig.sourceUserId ,
hostId : tunnelConfig.sourceHostId ,
tunnelName ,
marker : tunnelMarker ,
});
return ;
}
tunnelLogger . info ( "Remote tunnel process found, proceeding to kill" , {
operation : "tunnel_remote_found" ,
userId : tunnelConfig.sourceUserId ,
hostId : tunnelConfig.sourceHostId ,
tunnelName ,
marker : tunnelMarker ,
});
const killCmds = [
`pkill -TERM -f ' ${ tunnelMarker } '` ,
`sleep 2 && pkill -9 -f ' ${ tunnelMarker } '` ,
];
for ( const killCmd of killCmds ) {
try {
await execCommand ( client , killCmd );
} catch ( err ) {
tunnelLogger . warn (
`Kill command failed for ' ${ tunnelName } ': ${ ( err as Error ). message } ` ,
);
}
}
const verifyOutput = await execCommand ( client , checkCmd );
if ( verifyOutput ) {
tunnelLogger . warn (
`Some tunnel processes may still be running for ' ${ tunnelName } '` ,
);
} else {
tunnelLogger . success ( "Remote tunnel process killed" , {
operation : "tunnel_remote_killed" ,
userId : tunnelConfig.sourceUserId ,
hostId : tunnelConfig.sourceHostId ,
tunnelName ,
});
}
});
callback ();
} catch ( err ) {
tunnelLogger . error (
`Failed to connect to source host for killing tunnel ' ${ tunnelName } ': ${ ( err as Error ). message } ` ,
);
callback ( err as Error );
2025-12-31 22:20:12 -06:00
}
2025-08-07 02:20:27 -05:00
}
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/tunnel/status:
* get:
* summary: Get all tunnel statuses
* description: Retrieves the status of all SSH tunnels.
* tags:
* - SSH Tunnels
* responses:
* 200:
* description: A list of all tunnel statuses.
*/
2026-05-06 15:12:07 -05:00
2026-07-19 12:29:52 -05:00
export async function initializeAutoStartTunnels () : Promise < void > {
2025-09-12 14:42:00 -05:00
try {
2025-10-01 15:40:10 -05:00
const systemCrypto = SystemCrypto . getInstance ();
const internalAuthToken = await systemCrypto . getInternalAuthToken ();
2026-06-29 13:28:26 -05:00
const autostartHosts = await fetchInternalHosts (
"internal" ,
internalAuthToken ,
2025-09-12 14:42:00 -05:00
);
2026-06-29 13:28:26 -05:00
const allHosts = await fetchInternalHosts (
"internal/all" ,
internalAuthToken ,
2025-10-01 15:40:10 -05:00
);
2025-09-12 14:42:00 -05:00
const autoStartTunnels : TunnelConfig [] = [];
2025-10-01 15:40:10 -05:00
tunnelLogger . info (
`Found ${ autostartHosts . length } autostart hosts and ${ allHosts . length } total hosts for endpointHost resolution` ,
);
for ( const host of autostartHosts ) {
2025-09-12 14:42:00 -05:00
if ( host . enableTunnel && host . tunnelConnections ) {
for ( const tunnelConnection of host . tunnelConnections ) {
if ( tunnelConnection . autoStart ) {
2026-06-29 13:28:26 -05:00
const endpointHost = findHostByTunnelEndpoint (
allHosts ,
tunnelConnection . endpointHost ,
2025-09-12 14:42:00 -05:00
);
2026-06-29 13:28:26 -05:00
const mode =
tunnelConnection . mode || tunnelConnection . tunnelType || "remote" ;
const allowDirectTarget = mode !== "remote" ;
2025-08-07 02:20:27 -05:00
2026-06-29 13:28:26 -05:00
if ( endpointHost || allowDirectTarget ) {
2025-12-31 22:20:12 -06:00
const tunnelIndex =
host . tunnelConnections . indexOf ( tunnelConnection );
2025-09-12 14:42:00 -05:00
const tunnelConfig : TunnelConfig = {
2025-12-31 22:20:12 -06:00
name : normalizeTunnelName (
host . id ,
tunnelIndex ,
host . name || ` ${ host . username } @ ${ host . ip } ` ,
tunnelConnection . sourcePort ,
tunnelConnection . endpointHost ,
tunnelConnection . endpointPort ,
),
2026-05-06 15:12:07 -05:00
scope : tunnelConnection.scope || "s2s" ,
2026-06-29 13:28:26 -05:00
mode ,
2026-05-06 15:12:07 -05:00
bindHost : tunnelConnection.bindHost ,
targetHost : tunnelConnection.targetHost ,
2026-01-24 19:49:42 -06:00
tunnelType : tunnelConnection.tunnelType || "remote" ,
2025-12-31 22:20:12 -06:00
sourceHostId : host.id ,
tunnelIndex : tunnelIndex ,
2025-09-12 14:42:00 -05:00
hostName : host.name || ` ${ host . username } @ ${ host . ip } ` ,
sourceIP : host.ip ,
sourceSSHPort : host.port ,
sourceUsername : host.username ,
sourceAuthMethod : host.authType ,
sourceKeyType : host.keyType ,
2025-10-01 15:40:10 -05:00
sourceCredentialId : host.credentialId ,
sourceUserId : host.userId ,
2026-06-29 13:28:26 -05:00
endpointIP : endpointHost?.ip || tunnelConnection . endpointHost ,
endpointSSHPort : endpointHost?.port || 22 ,
endpointUsername : endpointHost?.username || "" ,
2025-12-31 22:20:12 -06:00
endpointHost : tunnelConnection.endpointHost ,
2025-10-01 15:40:10 -05:00
endpointAuthMethod :
2026-06-29 13:28:26 -05:00
tunnelConnection.endpointAuthType ||
endpointHost ? . authType ||
"none" ,
2025-10-01 15:40:10 -05:00
endpointKeyType :
2026-06-29 13:28:26 -05:00
tunnelConnection.endpointKeyType || endpointHost ? . keyType ,
endpointCredentialId : endpointHost?.credentialId ,
endpointUserId : endpointHost?.userId ,
2025-09-12 14:42:00 -05:00
sourcePort : tunnelConnection.sourcePort ,
endpointPort : tunnelConnection.endpointPort ,
maxRetries : tunnelConnection.maxRetries ,
retryInterval : tunnelConnection.retryInterval * 1000 ,
autoStart : tunnelConnection.autoStart ,
isPinned : host.pin ,
2025-12-31 22:20:12 -06:00
useSocks5 : host.useSocks5 ,
socks5Host : host.socks5Host ,
socks5Port : host.socks5Port ,
socks5Username : host.socks5Username ,
socks5Password : host.socks5Password ,
2025-09-12 14:42:00 -05:00
};
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
autoStartTunnels . push ( tunnelConfig );
2025-10-01 15:40:10 -05:00
} else {
tunnelLogger . error (
`Failed to find endpointHost ' ${ tunnelConnection . endpointHost } ' for tunnel from ${ host . name || ` ${ host . username } @ ${ host . ip } ` } . Available hosts: ${ allHosts . map (( h ) => h . name || ` ${ h . username } @ ${ h . ip } ` ). join ( ", " ) } ` ,
);
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
}
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
}
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
for ( const tunnelConfig of autoStartTunnels ) {
tunnelConfigs . set ( tunnelConfig . name , tunnelConfig );
setTimeout (() => {
connectSSHTunnel ( tunnelConfig , 0 ). catch (( error ) => {
tunnelLogger . error (
`Failed to connect tunnel ${ tunnelConfig . name } : ${ error instanceof Error ? error . message : "Unknown error" } ` ,
);
});
}, 1000 );
}
2025-11-05 10:36:16 -06:00
} catch ( error ) {
2025-09-12 14:42:00 -05:00
tunnelLogger . error (
"Failed to initialize auto-start tunnels:" ,
2025-11-05 10:36:16 -06:00
error instanceof Error ? error . message : "Unknown error" ,
2025-09-12 14:42:00 -05:00
);
}
2025-08-07 02:20:27 -05:00
}