mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-31 15:45:23 +00:00
fix: keep client tunnels open when one of their connections fails (Support#1417)
This commit is contained in:
1 parent
1466c7bcab
commit
db8305e393
2 files changed
+48
-15
No files matched your search
+11
-14
@@ -2187,11 +2187,6 @@ async function openC2SRelay(
|
|||||||
targetHost,
|
targetHost,
|
||||||
targetPort,
|
targetPort,
|
||||||
});
|
});
|
||||||
setC2STunnelStatus(tunnelName, {
|
|
||||||
connected: false,
|
|
||||||
status: "CONNECTING",
|
|
||||||
reason: `Opening relay to ${targetHost}:${targetPort}`,
|
|
||||||
});
|
|
||||||
const ws = new WebSocket(
|
const ws = new WebSocket(
|
||||||
relayUrl,
|
relayUrl,
|
||||||
getWebSocketOptions(relayUrl, { headers }),
|
getWebSocketOptions(relayUrl, { headers }),
|
||||||
@@ -2226,13 +2221,16 @@ async function openC2SRelay(
|
|||||||
|
|
||||||
socket.on("data", sendChunk);
|
socket.on("data", sendChunk);
|
||||||
socket.on("close", cleanup);
|
socket.on("close", cleanup);
|
||||||
|
// One connection failing must not stop the tunnel: the listener refuses
|
||||||
|
// new connections while the tunnel is not connected, and a browser opens
|
||||||
|
// many at once.
|
||||||
socket.on("error", (error) => {
|
socket.on("error", (error) => {
|
||||||
setC2STunnelError(tunnelName, error.message || "Local socket error");
|
logToFile(`[c2s] ${tunnelName} local socket error:`, error.message);
|
||||||
cleanup();
|
cleanup();
|
||||||
});
|
});
|
||||||
ws.on("close", cleanup);
|
ws.on("close", cleanup);
|
||||||
ws.on("error", (error) => {
|
ws.on("error", (error) => {
|
||||||
setC2STunnelError(tunnelName, error.message || "Relay connection failed");
|
logToFile(`[c2s] ${tunnelName} relay error:`, error.message);
|
||||||
cleanup();
|
cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2259,10 +2257,6 @@ async function openC2SRelay(
|
|||||||
if (message.type === "ready") {
|
if (message.type === "ready") {
|
||||||
ready = true;
|
ready = true;
|
||||||
logToFile(`[c2s] relay ready for ${tunnelName}`);
|
logToFile(`[c2s] relay ready for ${tunnelName}`);
|
||||||
setC2STunnelStatus(tunnelName, {
|
|
||||||
connected: true,
|
|
||||||
status: "CONNECTED",
|
|
||||||
});
|
|
||||||
if (initialData?.length) {
|
if (initialData?.length) {
|
||||||
ws.send(initialData);
|
ws.send(initialData);
|
||||||
}
|
}
|
||||||
@@ -2275,12 +2269,13 @@ async function openC2SRelay(
|
|||||||
"Relay rejected the client tunnel",
|
"Relay rejected the client tunnel",
|
||||||
);
|
);
|
||||||
logToFile("[c2s] relay error:", relayError);
|
logToFile("[c2s] relay error:", relayError);
|
||||||
setC2STunnelError(tunnelName, relayError);
|
if (relayError === C2S_REMOTE_SESSION_EXPIRED_ERROR) {
|
||||||
|
setC2STunnelError(tunnelName, relayError);
|
||||||
|
}
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logToFile("[c2s] invalid relay message:", error.message);
|
logToFile("[c2s] invalid relay message:", error.message);
|
||||||
setC2STunnelError(tunnelName, error.message || "Invalid relay response");
|
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2506,7 +2501,9 @@ function handleC2SLocalConnection(tunnel, socket, authToken) {
|
|||||||
"Local relay failed",
|
"Local relay failed",
|
||||||
);
|
);
|
||||||
logToFile("[c2s] local relay failed:", message);
|
logToFile("[c2s] local relay failed:", message);
|
||||||
setC2STunnelError(tunnelName, message);
|
if (message === C2S_REMOTE_SESSION_EXPIRED_ERROR) {
|
||||||
|
setC2STunnelError(tunnelName, message);
|
||||||
|
}
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,9 +72,12 @@ function setup() {
|
|||||||
runtimes.get(name).status = { connected: false, reason: error };
|
runtimes.get(name).status = { connected: false, reason: error };
|
||||||
},
|
},
|
||||||
createC2SFailure: (error: string) => ({ success: false, error }),
|
createC2SFailure: (error: string) => ({ success: false, error }),
|
||||||
|
normalizeC2SErrorMessage: (message: string) => message,
|
||||||
|
C2S_REMOTE_SESSION_EXPIRED_ERROR: "expired",
|
||||||
});
|
});
|
||||||
vm.runInContext(
|
vm.runInContext(
|
||||||
section("async function testC2SRelay(", "async function testC2STunnel(") +
|
section("async function openC2SRelay(", "async function testC2SRelay(") +
|
||||||
|
section("async function testC2SRelay(", "async function testC2STunnel(") +
|
||||||
section("async function startC2STunnel(", "function stopAllC2STunnels("),
|
section("async function startC2STunnel(", "function stopAllC2STunnels("),
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
@@ -140,4 +143,37 @@ describe("client tunnel runtime authentication", () => {
|
|||||||
).toEqual({ success: false, error: "Tunnel stopped" });
|
).toEqual({ success: false, error: "Tunnel stopped" });
|
||||||
expect(sockets).toHaveLength(1);
|
expect(sockets).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps the tunnel connected when one of its connections fails", async () => {
|
||||||
|
const { context, runtimes, sockets } = setup();
|
||||||
|
runtimes.set("web", { status: { connected: true, status: "CONNECTED" } });
|
||||||
|
const local = () =>
|
||||||
|
Object.assign(new EventEmitter(), {
|
||||||
|
destroy: vi.fn(),
|
||||||
|
write: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const first = local();
|
||||||
|
await context.openC2SRelay({ name: "web" }, "127.0.0.1", 8006, first);
|
||||||
|
await vi.waitFor(() => expect(sockets).toHaveLength(1));
|
||||||
|
first.emit("error", new Error("ECONNRESET"));
|
||||||
|
sockets[0].emit(
|
||||||
|
"message",
|
||||||
|
Buffer.from(JSON.stringify({ type: "error", error: "refused" })),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(runtimes.get("web").status.connected).toBe(true);
|
||||||
|
|
||||||
|
await context.openC2SRelay({ name: "web" }, "127.0.0.1", 8006, local());
|
||||||
|
await vi.waitFor(() => expect(sockets).toHaveLength(2));
|
||||||
|
sockets[1].emit(
|
||||||
|
"message",
|
||||||
|
Buffer.from(JSON.stringify({ type: "error", error: "expired" })),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(runtimes.get("web").status).toMatchObject({
|
||||||
|
connected: false,
|
||||||
|
reason: "expired",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user