diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 528f16c437..10ef4d358f 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -51,6 +51,7 @@ import { detachTunnelProfileLayer, tunnelProfileReferenceLayer, tunnelProfileSum import { insertSqliteRemoteTransportLayer, isSqliteRemoteTransportLayerType, sqliteRemoteTransportError } from "@/lib/connection/sqliteRemoteTransport"; import { sanitizeConnectionCredentials } from "@/lib/connection/credentialSanitizer"; import { applySshAuthMethod, inferSshAuthMethod } from "@/lib/connection/sshAuthMethod"; +import { editableSshLocalPort, normalizeSshLocalPort, sshLocalPortState, sshLocalPortSupportedForConnection } from "@/lib/connection/sshLocalPort"; import { applySshConfigHostAliasPrefill as prefillSshConfigHostAlias } from "@/lib/connection/sshConfigHosts"; import { canPersistConnectionTestResult, connectionEditDraftSyncAction } from "./connectionEditDraftSync"; import { createConnectionNoteVisibilityDraft, persistConnectionNoteVisibilityDraft as persistConnectionNoteVisibilityDraftState, resetConnectionNoteVisibilityDraft, setConnectionNoteVisibilityDraft, syncConnectionNoteVisibilityDraft } from "./connectionNoteVisibilityDraft"; @@ -555,6 +556,7 @@ function normalizeSshTunnel(hop: Partial): SshTunnelConfig { key_passphrase: hop.key_passphrase || "", connect_timeout_secs: Number(hop.connect_timeout_secs) || 5, expose_lan: !!hop.expose_lan, + local_port: hop.enabled === false ? normalizeSshLocalPort(hop.local_port) : editableSshLocalPort(hop.local_port), use_ssh_agent: !!hop.use_ssh_agent, ssh_agent_sock_path: hop.ssh_agent_sock_path || "", ssh_agent_key_fingerprint: hop.ssh_agent_key_fingerprint || "", @@ -6634,6 +6636,7 @@ function addHttpTunnel() { function duplicateTransportLayer(layer: TransportLayerConfig) { if (sqliteRemoteTransportRestricted.value && !isSqliteRemoteTransportLayerType(layer.type)) return; const next = normalizeTransportLayer({ ...layer, id: uuid(), name: layer.name ? `${layer.name} copy` : "" }); + if (next.type === "ssh") delete next.local_port; form.value.transport_layers = sqliteRemoteTransportRestricted.value ? insertSqliteRemoteTransportLayer(transportLayers.value, next) : [...transportLayers.value, next]; selectedTransportLayerId.value = next.id; resetTestState(); @@ -6719,11 +6722,19 @@ function updateSelectedSshAuthMethod(value: unknown) { resetTestState(); } +function updateSelectedSshLocalPort(value: unknown) { + const layer = selectedSshLayer.value; + if (!layer) return; + layer.local_port = editableSshLocalPort(value); + resetTestState(); +} + function validateTransportLayers(config: LegacyConnectionConfig) { const layers = config.transport_layers || []; if (config.db_type === "sqlite" && sqliteRemoteTransportError(layers)) { throw new Error(t("connection.sqliteTransportSshOnly")); } + const lastEnabledIndex = layers.reduce((last, layer, index) => (layer.enabled === false ? last : index), -1); layers.forEach((layer, index) => { if (layer.enabled === false) return; // Profile-referencing layers are stubs: the shared profile supplies the @@ -6753,6 +6764,10 @@ function validateTransportLayers(config: LegacyConnectionConfig) { if (!Number.isFinite(timeout) || timeout < 1 || timeout > 300) { throw new Error(t("connection.sshHopInvalidTimeout", { hop: label })); } + const localPortState = sshLocalPortState(layer.local_port); + if (localPortState === "invalid") throw new Error(t("connection.sshHopInvalidLocalPort", { hop: label })); + if (localPortState === "valid" && index !== lastEnabledIndex) throw new Error(t("connection.sshHopLocalPortNotLast", { hop: label })); + if (localPortState === "valid" && !sshLocalPortSupportedForConnection(config, mqSystemKind.value)) throw new Error(t("connection.sshHopLocalPortUnsupported", { hop: label })); } }); } @@ -11022,6 +11037,13 @@ function openExternalUrl(url: string) { {{ t("connection.sshExposeLan") }} +
+ +
+ +

{{ t("connection.sshLocalPortHint") }}

+
+