From 620c8690b5cc0723132eb24bdbee73d3dccfcd11 Mon Sep 17 00:00:00 2001 From: HabibAlmaula <39479500+HabibAlmaula@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:20:06 +0700 Subject: [PATCH] feat(ssh): support a fixed local port for SSH tunnel layers --- .../connection/ConnectionDialog.vue | 22 + .../ConnectionDialog.sshLocalPort.spec.ts | 334 +++++++++++ ...onnectionMutationRuntime.selection.spec.ts | 41 +- .../useSidebarConnectionMutationRuntime.ts | 3 +- .../src/i18n/__tests__/backendErrors.spec.ts | 12 + apps/desktop/src/i18n/backend-errors.ts | 2 + apps/desktop/src/i18n/locales/az.ts | 7 + apps/desktop/src/i18n/locales/en.ts | 7 + apps/desktop/src/i18n/locales/es.ts | 7 + apps/desktop/src/i18n/locales/id.ts | 7 + apps/desktop/src/i18n/locales/it.ts | 7 + apps/desktop/src/i18n/locales/ja.ts | 7 + apps/desktop/src/i18n/locales/ko.ts | 7 + apps/desktop/src/i18n/locales/pl.ts | 7 + apps/desktop/src/i18n/locales/pt-BR.ts | 7 + apps/desktop/src/i18n/locales/ru.ts | 7 + apps/desktop/src/i18n/locales/th.ts | 7 + apps/desktop/src/i18n/locales/tr.ts | 7 + apps/desktop/src/i18n/locales/zh-CN.ts | 7 + apps/desktop/src/i18n/locales/zh-TW.ts | 7 + .../connection/__tests__/sshLocalPort.spec.ts | 113 ++++ .../connectionConfigTransfer.spec.ts | 12 + .../src/lib/connection/sshLocalPort.ts | 59 ++ apps/desktop/src/stores/connectionStore.ts | 3 +- apps/desktop/src/types/database.ts | 8 + .../src/connection/connection_secrets.rs | 1 + crates/dbx-core/src/connection/mod.rs | 397 ++++++++++++- crates/dbx-core/src/persistence/cloud_sync.rs | 2 + crates/dbx-core/src/persistence/storage.rs | 6 + crates/dbx-driver-support/src/ssh_config.rs | 2 + crates/dbx-drivers/src/db/ssh_tunnel.rs | 553 +++++++++++++++--- .../src/db/transport_layer_tunnel.rs | 1 + crates/dbx-mcp/src/backend.rs | 66 ++- crates/dbx-types/src/models/connection.rs | 40 ++ crates/dbx-web/src/main.rs | 2 + docs/content/docs/ssh-tunnel.cn.mdx | 20 +- docs/content/docs/ssh-tunnel.mdx | 20 +- .../app-tests/connectionClipboard.test.ts | 16 +- src-tauri/src/lib.rs | 1 + 39 files changed, 1732 insertions(+), 102 deletions(-) create mode 100644 apps/desktop/src/components/connection/__tests__/ConnectionDialog.sshLocalPort.spec.ts create mode 100644 apps/desktop/src/lib/connection/__tests__/sshLocalPort.spec.ts create mode 100644 apps/desktop/src/lib/connection/sshLocalPort.ts 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") }}

+
+