diff --git a/src/backend/ssh/tunnel.ts b/src/backend/ssh/tunnel.ts index 632fa109..e83cf391 100644 --- a/src/backend/ssh/tunnel.ts +++ b/src/backend/ssh/tunnel.ts @@ -828,15 +828,22 @@ async function connectSSHTunnel( return; } + const tunnelType = tunnelConfig.tunnelType || "remote"; + const tunnelFlag = tunnelType === "local" ? "-L" : "-R"; + const portMapping = + tunnelType === "local" + ? `${tunnelConfig.sourcePort}:${tunnelConfig.endpointIP}:${tunnelConfig.endpointPort}` + : `${tunnelConfig.endpointPort}:localhost:${tunnelConfig.sourcePort}`; + let tunnelCmd: string; if ( resolvedEndpointCredentials.authMethod === "key" && resolvedEndpointCredentials.sshKey ) { const keyFilePath = `/tmp/tunnel_key_${tunnelName.replace(/[^a-zA-Z0-9]/g, "_")}`; - tunnelCmd = `echo '${resolvedEndpointCredentials.sshKey}' > ${keyFilePath} && chmod 600 ${keyFilePath} && exec -a "${tunnelMarker}" ssh -i ${keyFilePath} -N -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o GatewayPorts=yes -R ${tunnelConfig.endpointPort}:localhost:${tunnelConfig.sourcePort} ${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP} && rm -f ${keyFilePath}`; + tunnelCmd = `echo '${resolvedEndpointCredentials.sshKey}' > ${keyFilePath} && chmod 600 ${keyFilePath} && exec -a "${tunnelMarker}" ssh -i ${keyFilePath} -N -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o GatewayPorts=yes ${tunnelFlag} ${portMapping} ${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP} && rm -f ${keyFilePath}`; } else { - tunnelCmd = `exec -a "${tunnelMarker}" sshpass -p '${resolvedEndpointCredentials.password || ""}' ssh -N -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o GatewayPorts=yes -R ${tunnelConfig.endpointPort}:localhost:${tunnelConfig.sourcePort} ${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}`; + tunnelCmd = `exec -a "${tunnelMarker}" sshpass -p '${resolvedEndpointCredentials.password || ""}' ssh -N -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o GatewayPorts=yes ${tunnelFlag} ${portMapping} ${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}`; } conn.exec(tunnelCmd, (err, stream) => { @@ -1302,7 +1309,9 @@ async function killRemoteTunnelByMarker( } conn.on("ready", () => { - const checkCmd = `ps aux | grep -E '(${tunnelMarker}|ssh.*-R.*${tunnelConfig.endpointPort}:localhost:${tunnelConfig.sourcePort}.*${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}|sshpass.*ssh.*-R.*${tunnelConfig.endpointPort})' | grep -v grep`; + const tunnelType = tunnelConfig.tunnelType || "remote"; + const tunnelFlag = tunnelType === "local" ? "-L" : "-R"; + const checkCmd = `ps aux | grep -E '(${tunnelMarker}|ssh.*${tunnelFlag}.*${tunnelConfig.endpointPort}:.*:${tunnelConfig.sourcePort}.*${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}|sshpass.*ssh.*${tunnelFlag})' | grep -v grep`; conn.exec(checkCmd, (_err, stream) => { let foundProcesses = false; @@ -1323,8 +1332,8 @@ async function killRemoteTunnelByMarker( const killCmds = [ `pkill -TERM -f '${tunnelMarker}'`, - `sleep 1 && pkill -f 'ssh.*-R.*${tunnelConfig.endpointPort}:localhost:${tunnelConfig.sourcePort}.*${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}'`, - `sleep 1 && pkill -f 'sshpass.*ssh.*-R.*${tunnelConfig.endpointPort}'`, + `sleep 1 && pkill -f 'ssh.*${tunnelFlag}.*${tunnelConfig.endpointPort}:.*:${tunnelConfig.sourcePort}.*${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}'`, + `sleep 1 && pkill -f 'sshpass.*ssh.*${tunnelFlag}.*${tunnelConfig.endpointPort}'`, `sleep 2 && pkill -9 -f '${tunnelMarker}'`, ]; @@ -1929,6 +1938,7 @@ async function initializeAutoStartTunnels(): Promise { tunnelConnection.endpointHost, tunnelConnection.endpointPort, ), + tunnelType: tunnelConnection.tunnelType || "remote", sourceHostId: host.id, tunnelIndex: tunnelIndex, hostName: host.name || `${host.username}@${host.ip}`, diff --git a/src/locales/en.json b/src/locales/en.json index 20263698..ed97ae63 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -891,6 +891,13 @@ "autoStartContainer": "Auto Start on Container Launch", "autoStartDesc": "Automatically start this tunnel when the container launches", "addConnection": "Add Tunnel Connection", + "tunnelType": "Tunnel Type", + "tunnelTypeLocal": "Local (-L)", + "tunnelTypeRemote": "Remote (-R)", + "tunnelTypeLocalDesc": "Forward local port to remote endpoint", + "tunnelTypeRemoteDesc": "Forward remote port to local machine", + "tunnelForwardDescriptionLocal": "This tunnel will forward traffic from local port {{sourcePort}} to port {{endpointPort}} on the endpoint machine.", + "tunnelForwardDescriptionRemote": "This tunnel will forward traffic from port {{sourcePort}} on the source machine (current connection details in general tab) to port {{endpointPort}} on the endpoint machine.", "sshpassRequired": "Sshpass Required For Password Authentication", "sshpassRequiredDesc": "For password authentication in tunnels, sshpass must be installed on the system.", "otherInstallMethods": "Other installation methods:", diff --git a/src/types/index.ts b/src/types/index.ts index 045aeb31..640f2b62 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -203,6 +203,7 @@ export interface CredentialData { // ============================================================================ export interface TunnelConnection { + tunnelType?: "local" | "remote"; sourcePort: number; endpointPort: number; endpointHost: string; @@ -220,6 +221,7 @@ export interface TunnelConnection { export interface TunnelConfig { name: string; + tunnelType?: "local" | "remote"; sourceHostId: number; tunnelIndex: number; diff --git a/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx b/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx index 29853b6b..1351ec31 100644 --- a/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx @@ -293,9 +293,18 @@ export function HostManagerEditor({ tunnelConnections: z .array( z.object({ + tunnelType: z + .enum(["local", "remote"]) + .default("remote") + .optional(), sourcePort: z.coerce.number().min(1).max(65535), endpointPort: z.coerce.number().min(1).max(65535), endpointHost: z.string().min(1), + endpointPassword: z.string().optional(), + endpointKey: z.string().optional(), + endpointKeyPassword: z.string().optional(), + endpointAuthType: z.string().optional(), + endpointKeyType: z.string().optional(), maxRetries: z.coerce.number().min(0).max(100).default(3), retryInterval: z.coerce.number().min(1).max(3600).default(10), autoStart: z.boolean().default(false), @@ -667,7 +676,10 @@ export function HostManagerEditor({ enableFileManager: Boolean(cleanedHost.enableFileManager), defaultPath: cleanedHost.defaultPath || "/", tunnelConnections: Array.isArray(cleanedHost.tunnelConnections) - ? cleanedHost.tunnelConnections + ? cleanedHost.tunnelConnections.map((conn: any) => ({ + ...conn, + tunnelType: conn.tunnelType || "remote", + })) : [], jumpHosts: Array.isArray(cleanedHost.jumpHosts) ? cleanedHost.jumpHosts diff --git a/src/ui/desktop/apps/host-manager/hosts/tabs/HostTunnelTab.tsx b/src/ui/desktop/apps/host-manager/hosts/tabs/HostTunnelTab.tsx index 9cc96355..144e905a 100644 --- a/src/ui/desktop/apps/host-manager/hosts/tabs/HostTunnelTab.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/tabs/HostTunnelTab.tsx @@ -142,6 +142,57 @@ export function HostTunnelTab({ {t("hosts.remove")} +
+ ( + + {t("hosts.tunnelType")} + +
+ + +
+
+
+ )} + /> +

- {t("hosts.tunnelForwardDescription", { - sourcePort: - form.watch( - `tunnelConnections.${index}.sourcePort`, - ) || "22", - endpointPort: - form.watch( - `tunnelConnections.${index}.endpointPort`, - ) || "224", - })} + {form.watch( + `tunnelConnections.${index}.tunnelType`, + ) === "local" + ? t("hosts.tunnelForwardDescriptionLocal", { + sourcePort: + form.watch( + `tunnelConnections.${index}.sourcePort`, + ) || "22", + endpointPort: + form.watch( + `tunnelConnections.${index}.endpointPort`, + ) || "224", + }) + : t("hosts.tunnelForwardDescriptionRemote", { + sourcePort: + form.watch( + `tunnelConnections.${index}.sourcePort`, + ) || "22", + endpointPort: + form.watch( + `tunnelConnections.${index}.endpointPort`, + ) || "224", + })}

@@ -337,6 +401,7 @@ export function HostTunnelTab({ field.onChange([ ...field.value, { + tunnelType: "remote", sourcePort: 22, endpointPort: 224, endpointHost: "",