diff --git a/CHANGELOG.md b/CHANGELOG.md index 522752685..4052a8501 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ - Indonesian uses the same technical terms everywhere - When the server was down or still starting, the login page said it could not connect to the database and stopped trying. It now says the server is not responding and continues on its own once the server is back - nginx answered 404 instead of 502 while the backend was down +- A client tunnel that could not start always said "Endpoint SSH connection closed" on the desktop app instead of the real reason, like the target refusing the connection - The Docker server crashed and restarted every few hours in ssh2's native crypto on Node 24 ## 26.10.1 diff --git a/electron/main.cjs b/electron/main.cjs index 1f6867c8f..bf54001c3 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -2389,6 +2389,8 @@ async function testC2SRelay( if (runtime) runtime.authSocket = ws; return new Promise((resolve) => { let settled = false; + // The relay sends its reason and then closes; keep the reason. + let relayError = null; const settle = (result) => { if (settled) return; settled = true; @@ -2425,6 +2427,10 @@ async function testC2SRelay( settle({ success: true }); } else if (message.type === "error") { clearTimeout(timer); + relayError = normalizeC2SErrorMessage( + message.error, + "Tunnel test failed", + ); settle(createC2SFailure(message.error, "Tunnel test failed")); } } catch (error) { @@ -2438,7 +2444,10 @@ async function testC2SRelay( }); ws.on("close", () => { if (runtime && c2sTunnelRuntimes.get(tunnel.name) === runtime) { - setC2STunnelError(tunnel.name, "Endpoint SSH connection closed"); + setC2STunnelError( + tunnel.name, + relayError || "Endpoint SSH connection closed", + ); } clearTimeout(timer); settle({ success: false, error: "Tunnel test connection closed" }); diff --git a/src/backend/tests/electron/c2s-runtime.test.ts b/src/backend/tests/electron/c2s-runtime.test.ts index febca3e4d..a65c224a7 100644 --- a/src/backend/tests/electron/c2s-runtime.test.ts +++ b/src/backend/tests/electron/c2s-runtime.test.ts @@ -145,6 +145,35 @@ describe("client tunnel runtime authentication", () => { expect(sockets).toHaveLength(1); }); + it("keeps the relay's reason when it closes after an error", async () => { + const { context, runtimes, sockets } = setup(); + const runtime = { status: { connected: true } }; + runtimes.set("web", runtime); + const test = context.testC2SRelay( + { name: "web" }, + "127.0.0.1", + 8443, + null, + runtime, + ); + await vi.waitFor(() => expect(sockets).toHaveLength(1)); + sockets[0].emit( + "message", + Buffer.from( + JSON.stringify({ + type: "error", + error: "Target 127.0.0.1:8443 refused", + }), + ), + false, + ); + sockets[0].close(); + expect(await test).toMatchObject({ success: false }); + expect(runtimes.get("web").status.reason).toBe( + "Target 127.0.0.1:8443 refused", + ); + }); + 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" } });