feat: convert ssh tunnels into a plugin

This commit is contained in:
LukeGus committed 2026-09-23 17:27:28 -05:00
1 parent c85c1fdb86
commit 1682861853
220 files changed
+36928 -11374

No files matched your search

@@ -0,0 +1,76 @@
/**
* Tunnel names, shared by the backend and every frontend view so the two
* sides can never build the same tunnel under different names.
*/
/** A host's saved server tunnel: `<hostId>::<index>::<label>::<port>::<endpoint>::<port>`. */
export function buildTunnelName(
hostId: number | string,
tunnelIndex: number,
hostLabel: string,
sourcePort: number | string,
endpointHost: string,
endpointPort: number | string,
): string {
return `${hostId}::${tunnelIndex}::${hostLabel}::${sourcePort}::${endpointHost}::${endpointPort}`;
}
/** The label a server tunnel name carries for its source host. */
export function tunnelHostLabel(host: {
name?: string | null;
username?: string | null;
ip: string;
}): string {
return host.name || `${host.username ?? ""}@${host.ip}`;
}
/** A saved server tunnel's name, from its host and position in the list. */
export function serverTunnelName(
host: {
id: number | string;
name?: string | null;
username?: string | null;
ip: string;
},
index: number,
tunnel: {
sourcePort: number | string;
endpointHost?: string | null;
endpointPort?: number | string | null;
},
): string {
return buildTunnelName(
host.id,
index,
tunnelHostLabel(host),
tunnel.sourcePort,
(tunnel.endpointHost ?? "").trim(),
tunnel.endpointPort ?? 0,
);
}
/** A desktop client tunnel's name, as the Electron main process tracks it. */
export function clientTunnelName(
tunnel: {
sourceHostId?: number;
mode?: string;
tunnelType?: string;
localAddress?: string;
remoteAddress?: string;
sourcePort: number;
endpointPort?: number;
},
index: number,
): string {
const address = (value?: string) => value?.trim() || "127.0.0.1";
return [
"c2s",
index,
tunnel.sourceHostId || 0,
tunnel.mode || tunnel.tunnelType || "local",
address(tunnel.localAddress),
address(tunnel.remoteAddress),
tunnel.sourcePort,
tunnel.endpointPort || 0,
].join("::");
}
+88
View File
@@ -0,0 +1,88 @@
export type TunnelScope = "s2s" | "c2s";
export type TunnelMode = "local" | "remote" | "dynamic";
export const CONNECTION_STATES = {
DISCONNECTED: "disconnected",
CONNECTING: "connecting",
CONNECTED: "connected",
VERIFYING: "verifying",
FAILED: "failed",
UNSTABLE: "unstable",
RETRYING: "retrying",
WAITING: "waiting",
DISCONNECTING: "disconnecting",
} as const;
export type ConnectionState =
(typeof CONNECTION_STATES)[keyof typeof CONNECTION_STATES];
export type TunnelErrorType =
| "CONNECTION_FAILED"
| "AUTHENTICATION_FAILED"
| "TIMEOUT"
| "NETWORK_ERROR"
| "UNKNOWN";
/** One saved tunnel, as a host's tunnelConnections setting or a C2S preset holds it. */
export interface TunnelConnection {
scope?: TunnelScope;
mode?: TunnelMode;
tunnelType?: "local" | "remote";
localAddress?: string;
remoteAddress?: string;
bindHost?: string;
sourceHostId?: number;
sourceHostSyncId?: string;
sourceHostName?: string;
sourcePort: number;
endpointPort: number;
endpointHost?: string;
targetHost?: string;
/** Seconds. */
retryInterval: number;
maxRetries: number;
autoStart: boolean;
displayName?: string;
}
/** What POST /connect takes. Credentials are always resolved server side. */
export interface TunnelConnectRequest {
name: string;
sourceHostId: number;
tunnelIndex: number;
scope?: TunnelScope;
mode?: TunnelMode;
tunnelType?: "local" | "remote";
bindHost?: string;
targetHost?: string;
endpointHost?: string;
sourcePort: number;
endpointPort: number;
maxRetries?: number;
/** Seconds. */
retryInterval?: number;
autoStart?: boolean;
}
export interface TunnelStatus {
connected: boolean;
status: ConnectionState;
retryCount?: number;
maxRetries?: number;
nextRetryIn?: number;
reason?: string;
errorType?: TunnelErrorType;
manualDisconnect?: boolean;
retryExhausted?: boolean;
}
export interface C2STunnelPreset {
id: number;
userId: string;
name: string;
config: TunnelConnection[];
platform?: string | null;
computerName?: string | null;
createdAt: string;
updatedAt: string;
}