feat: added -r and -l support for tunnels
This commit is contained in:
@@ -828,15 +828,22 @@ async function connectSSHTunnel(
|
|||||||
return;
|
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;
|
let tunnelCmd: string;
|
||||||
if (
|
if (
|
||||||
resolvedEndpointCredentials.authMethod === "key" &&
|
resolvedEndpointCredentials.authMethod === "key" &&
|
||||||
resolvedEndpointCredentials.sshKey
|
resolvedEndpointCredentials.sshKey
|
||||||
) {
|
) {
|
||||||
const keyFilePath = `/tmp/tunnel_key_${tunnelName.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
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 {
|
} 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) => {
|
conn.exec(tunnelCmd, (err, stream) => {
|
||||||
@@ -1302,7 +1309,9 @@ async function killRemoteTunnelByMarker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
conn.on("ready", () => {
|
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) => {
|
conn.exec(checkCmd, (_err, stream) => {
|
||||||
let foundProcesses = false;
|
let foundProcesses = false;
|
||||||
@@ -1323,8 +1332,8 @@ async function killRemoteTunnelByMarker(
|
|||||||
|
|
||||||
const killCmds = [
|
const killCmds = [
|
||||||
`pkill -TERM -f '${tunnelMarker}'`,
|
`pkill -TERM -f '${tunnelMarker}'`,
|
||||||
`sleep 1 && pkill -f 'ssh.*-R.*${tunnelConfig.endpointPort}:localhost:${tunnelConfig.sourcePort}.*${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}'`,
|
`sleep 1 && pkill -f 'ssh.*${tunnelFlag}.*${tunnelConfig.endpointPort}:.*:${tunnelConfig.sourcePort}.*${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}'`,
|
||||||
`sleep 1 && pkill -f 'sshpass.*ssh.*-R.*${tunnelConfig.endpointPort}'`,
|
`sleep 1 && pkill -f 'sshpass.*ssh.*${tunnelFlag}.*${tunnelConfig.endpointPort}'`,
|
||||||
`sleep 2 && pkill -9 -f '${tunnelMarker}'`,
|
`sleep 2 && pkill -9 -f '${tunnelMarker}'`,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -1929,6 +1938,7 @@ async function initializeAutoStartTunnels(): Promise<void> {
|
|||||||
tunnelConnection.endpointHost,
|
tunnelConnection.endpointHost,
|
||||||
tunnelConnection.endpointPort,
|
tunnelConnection.endpointPort,
|
||||||
),
|
),
|
||||||
|
tunnelType: tunnelConnection.tunnelType || "remote",
|
||||||
sourceHostId: host.id,
|
sourceHostId: host.id,
|
||||||
tunnelIndex: tunnelIndex,
|
tunnelIndex: tunnelIndex,
|
||||||
hostName: host.name || `${host.username}@${host.ip}`,
|
hostName: host.name || `${host.username}@${host.ip}`,
|
||||||
|
|||||||
@@ -891,6 +891,13 @@
|
|||||||
"autoStartContainer": "Auto Start on Container Launch",
|
"autoStartContainer": "Auto Start on Container Launch",
|
||||||
"autoStartDesc": "Automatically start this tunnel when the container launches",
|
"autoStartDesc": "Automatically start this tunnel when the container launches",
|
||||||
"addConnection": "Add Tunnel Connection",
|
"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",
|
"sshpassRequired": "Sshpass Required For Password Authentication",
|
||||||
"sshpassRequiredDesc": "For password authentication in tunnels, sshpass must be installed on the system.",
|
"sshpassRequiredDesc": "For password authentication in tunnels, sshpass must be installed on the system.",
|
||||||
"otherInstallMethods": "Other installation methods:",
|
"otherInstallMethods": "Other installation methods:",
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ export interface CredentialData {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export interface TunnelConnection {
|
export interface TunnelConnection {
|
||||||
|
tunnelType?: "local" | "remote";
|
||||||
sourcePort: number;
|
sourcePort: number;
|
||||||
endpointPort: number;
|
endpointPort: number;
|
||||||
endpointHost: string;
|
endpointHost: string;
|
||||||
@@ -220,6 +221,7 @@ export interface TunnelConnection {
|
|||||||
|
|
||||||
export interface TunnelConfig {
|
export interface TunnelConfig {
|
||||||
name: string;
|
name: string;
|
||||||
|
tunnelType?: "local" | "remote";
|
||||||
|
|
||||||
sourceHostId: number;
|
sourceHostId: number;
|
||||||
tunnelIndex: number;
|
tunnelIndex: number;
|
||||||
|
|||||||
@@ -293,9 +293,18 @@ export function HostManagerEditor({
|
|||||||
tunnelConnections: z
|
tunnelConnections: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
|
tunnelType: z
|
||||||
|
.enum(["local", "remote"])
|
||||||
|
.default("remote")
|
||||||
|
.optional(),
|
||||||
sourcePort: z.coerce.number().min(1).max(65535),
|
sourcePort: z.coerce.number().min(1).max(65535),
|
||||||
endpointPort: z.coerce.number().min(1).max(65535),
|
endpointPort: z.coerce.number().min(1).max(65535),
|
||||||
endpointHost: z.string().min(1),
|
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),
|
maxRetries: z.coerce.number().min(0).max(100).default(3),
|
||||||
retryInterval: z.coerce.number().min(1).max(3600).default(10),
|
retryInterval: z.coerce.number().min(1).max(3600).default(10),
|
||||||
autoStart: z.boolean().default(false),
|
autoStart: z.boolean().default(false),
|
||||||
@@ -667,7 +676,10 @@ export function HostManagerEditor({
|
|||||||
enableFileManager: Boolean(cleanedHost.enableFileManager),
|
enableFileManager: Boolean(cleanedHost.enableFileManager),
|
||||||
defaultPath: cleanedHost.defaultPath || "/",
|
defaultPath: cleanedHost.defaultPath || "/",
|
||||||
tunnelConnections: Array.isArray(cleanedHost.tunnelConnections)
|
tunnelConnections: Array.isArray(cleanedHost.tunnelConnections)
|
||||||
? cleanedHost.tunnelConnections
|
? cleanedHost.tunnelConnections.map((conn: any) => ({
|
||||||
|
...conn,
|
||||||
|
tunnelType: conn.tunnelType || "remote",
|
||||||
|
}))
|
||||||
: [],
|
: [],
|
||||||
jumpHosts: Array.isArray(cleanedHost.jumpHosts)
|
jumpHosts: Array.isArray(cleanedHost.jumpHosts)
|
||||||
? cleanedHost.jumpHosts
|
? cleanedHost.jumpHosts
|
||||||
|
|||||||
@@ -142,6 +142,57 @@ export function HostTunnelTab({
|
|||||||
{t("hosts.remove")}
|
{t("hosts.remove")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`tunnelConnections.${index}.tunnelType`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("hosts.tunnelType")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex gap-6">
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
value="local"
|
||||||
|
checked={field.value === "local"}
|
||||||
|
onChange={() => field.onChange("local")}
|
||||||
|
className="w-4 h-4 text-primary border-input focus:ring-ring"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{t("hosts.tunnelTypeLocal")}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{t("hosts.tunnelTypeLocalDesc")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
value="remote"
|
||||||
|
checked={field.value === "remote"}
|
||||||
|
onChange={() =>
|
||||||
|
field.onChange("remote")
|
||||||
|
}
|
||||||
|
className="w-4 h-4 text-primary border-input focus:ring-ring"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{t("hosts.tunnelTypeRemote")}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{t("hosts.tunnelTypeRemoteDesc")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-12 gap-4">
|
<div className="grid grid-cols-12 gap-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -254,7 +305,20 @@ export function HostTunnelTab({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-sm text-muted-foreground mt-2">
|
<p className="text-sm text-muted-foreground mt-2">
|
||||||
{t("hosts.tunnelForwardDescription", {
|
{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:
|
sourcePort:
|
||||||
form.watch(
|
form.watch(
|
||||||
`tunnelConnections.${index}.sourcePort`,
|
`tunnelConnections.${index}.sourcePort`,
|
||||||
@@ -337,6 +401,7 @@ export function HostTunnelTab({
|
|||||||
field.onChange([
|
field.onChange([
|
||||||
...field.value,
|
...field.value,
|
||||||
{
|
{
|
||||||
|
tunnelType: "remote",
|
||||||
sourcePort: 22,
|
sourcePort: 22,
|
||||||
endpointPort: 224,
|
endpointPort: 224,
|
||||||
endpointHost: "",
|
endpointHost: "",
|
||||||
|
|||||||
Reference in New Issue
Block a user