Fix synced client tunnel endpoints (#1302)

This commit is contained in:
ZacharyZcR
2026-08-23 22:55:45 +08:00
committed by GitHub
parent 780cfb58e2
commit c42cd40a2c
4 changed files with 72 additions and 8 deletions
+27 -7
View File
@@ -14,6 +14,7 @@ import {
} from "./ssh-primitives.js";
import { sendC2SMessage, writeC2SRemoteChunk } from "./c2s-relay-utils.js";
import { getTunnelMode } from "./utils.js";
import { createCurrentHostResolutionRepository } from "../../database/repositories/factory.js";
export type C2SOpenMessage = {
type: "open" | "test";
@@ -25,17 +26,36 @@ export type C2SOpenMessage = {
const permissionManager = PermissionManager.getInstance();
let c2sRemoteStreamCounter = 0;
export async function resolveC2SSourceHostId(
tunnelConfig: Partial<TunnelConfig>,
findHostIdBySyncId: (syncId: string) => Promise<number | null>,
): Promise<number> {
const sourceHostSyncId = tunnelConfig.sourceHostSyncId?.trim();
if (sourceHostSyncId) {
const remoteHostId = await findHostIdBySyncId(sourceHostSyncId);
if (!remoteHostId) {
throw new Error("Endpoint SSH host was not found on the remote server");
}
return remoteHostId;
}
if (!tunnelConfig.sourceHostId) {
throw new Error("Endpoint SSH host is required");
}
return tunnelConfig.sourceHostId;
}
async function resolveC2STunnelSource(
tunnelConfig: Partial<TunnelConfig>,
userId: string,
): Promise<TunnelConfig> {
if (!tunnelConfig.sourceHostId) {
throw new Error("Endpoint SSH host is required");
}
const sourceHostId = await resolveC2SSourceHostId(tunnelConfig, (syncId) =>
createCurrentHostResolutionRepository().findHostIdBySyncId(syncId),
);
const accessInfo = await permissionManager.canAccessHost(
userId,
tunnelConfig.sourceHostId,
sourceHostId,
"connect",
);
if (!accessInfo.hasAccess) {
@@ -43,13 +63,13 @@ async function resolveC2STunnelSource(
}
const { resolveHostById } = await import("../host-resolver.js");
const resolvedHost = await resolveHostById(tunnelConfig.sourceHostId, userId);
const resolvedHost = await resolveHostById(sourceHostId, userId);
if (!resolvedHost) {
throw new Error("Endpoint SSH host not found");
}
return {
name: tunnelConfig.name || `c2s:${tunnelConfig.sourceHostId}`,
name: tunnelConfig.name || `c2s:${sourceHostId}`,
scope: "c2s",
mode: tunnelConfig.mode || "local",
tunnelType:
@@ -57,7 +77,7 @@ async function resolveC2STunnelSource(
(tunnelConfig.mode === "remote" ? "remote" : "local"),
bindHost: tunnelConfig.bindHost,
targetHost: tunnelConfig.targetHost || "127.0.0.1",
sourceHostId: resolvedHost.id || tunnelConfig.sourceHostId,
sourceHostId: resolvedHost.id || sourceHostId,
tunnelIndex: tunnelConfig.tunnelIndex || 0,
requestingUserId: userId,
hostName:
@@ -0,0 +1,31 @@
import { describe, expect, it, vi } from "vitest";
import { resolveC2SSourceHostId } from "../../../hosts/tunnel/c2s-relay.js";
describe("resolveC2SSourceHostId", () => {
it("uses the remote row matching the stable sync id", async () => {
const findBySyncId = vi.fn().mockResolvedValue(42);
await expect(
resolveC2SSourceHostId(
{ sourceHostId: 7, sourceHostSyncId: "host-sync-id" },
findBySyncId,
),
).resolves.toBe(42);
expect(findBySyncId).toHaveBeenCalledWith("host-sync-id");
});
it("keeps legacy local ids when no sync id is available", async () => {
await expect(
resolveC2SSourceHostId({ sourceHostId: 7 }, vi.fn()),
).resolves.toBe(7);
});
it("does not silently fall back to a mismatched id", async () => {
await expect(
resolveC2SSourceHostId(
{ sourceHostId: 7, sourceHostSyncId: "missing" },
vi.fn().mockResolvedValue(null),
),
).rejects.toThrow("not found on the remote server");
});
});
+2
View File
@@ -482,6 +482,7 @@ export interface TunnelConnection {
tunnelType?: "local" | "remote";
bindHost?: string;
sourceHostId?: number;
sourceHostSyncId?: string;
sourceHostName?: string;
sourcePort: number;
endpointPort: number;
@@ -508,6 +509,7 @@ export interface TunnelConfig {
targetHost?: string;
sourceHostId: number;
sourceHostSyncId?: string;
tunnelIndex: number;
requestingUserId?: string;
+12 -1
View File
@@ -347,10 +347,20 @@ export function C2STunnelPresetManager(): React.ReactElement {
getSSHHosts(),
]);
setHosts(nextHosts);
const hostsById = new Map(nextHosts.map((host) => [host.id, host]));
const normalizedConfig = Array.isArray(config)
? (config as TunnelConnection[])
.filter((tunnel) => tunnel.scope === "c2s")
.map(normalizeClientTunnel)
.map((tunnel) => {
const sourceHost = tunnel.sourceHostId
? hostsById.get(tunnel.sourceHostId)
: undefined;
return normalizeClientTunnel({
...tunnel,
sourceHostSyncId:
tunnel.sourceHostSyncId || sourceHost?.syncId || undefined,
});
})
: [];
setLocalConfig(normalizedConfig);
setSavedLocalConfig(normalizedConfig);
@@ -473,6 +483,7 @@ export function C2STunnelPresetManager(): React.ReactElement {
if (!host) return;
updateTunnel(index, {
sourceHostId: host.id,
sourceHostSyncId: host.syncId || undefined,
sourceHostName: host.name,
endpointHost: host.name,
endpointPort: 22,