Fix status checks through jump hosts (#1045)

This commit is contained in:
ZacharyZcR
2026-07-14 01:14:20 +08:00
committed by GitHub
parent c6a2ac69dc
commit d44695eb79
3 changed files with 114 additions and 9 deletions
+57 -1
View File
@@ -1,8 +1,10 @@
import { describe, it, expect } from "vitest"; import type { Client } from "ssh2";
import { describe, it, expect, vi } from "vitest";
import { import {
supportsMetrics, supportsMetrics,
isTcpPingEnabled, isTcpPingEnabled,
createConnectionLog, createConnectionLog,
tcpPingThroughJumpHost,
} from "./host-metrics-helpers.js"; } from "./host-metrics-helpers.js";
describe("supportsMetrics", () => { describe("supportsMetrics", () => {
@@ -66,3 +68,57 @@ describe("createConnectionLog", () => {
expect("timestamp" in entry).toBe(false); expect("timestamp" in entry).toBe(false);
}); });
}); });
describe("tcpPingThroughJumpHost", () => {
it("reports the final destination online when forwarding succeeds", async () => {
const stream = { destroy: vi.fn() };
const jumpClient = {
end: vi.fn(),
forwardOut: vi.fn((_src, _srcPort, host, port, callback) => {
expect(host).toBe("private.example");
expect(port).toBe(22);
callback(undefined, stream);
}),
} as unknown as Pick<Client, "forwardOut" | "end">;
await expect(
tcpPingThroughJumpHost(jumpClient, "private.example", 22),
).resolves.toBe(true);
expect(stream.destroy).toHaveBeenCalledOnce();
expect(jumpClient.end).toHaveBeenCalledOnce();
});
it("reports the final destination offline when forwarding fails", async () => {
const jumpClient = {
end: vi.fn(),
forwardOut: vi.fn((_src, _srcPort, _host, _port, callback) => {
callback(new Error("Connection refused"));
}),
} as unknown as Pick<Client, "forwardOut" | "end">;
await expect(
tcpPingThroughJumpHost(jumpClient, "private.example", 22),
).resolves.toBe(false);
expect(jumpClient.end).toHaveBeenCalledOnce();
});
it("reports the final destination offline when forwarding times out", async () => {
vi.useFakeTimers();
const jumpClient = {
end: vi.fn(),
forwardOut: vi.fn(),
} as unknown as Pick<Client, "forwardOut" | "end">;
const result = tcpPingThroughJumpHost(
jumpClient,
"private.example",
22,
5000,
);
await vi.advanceTimersByTimeAsync(5000);
await expect(result).resolves.toBe(false);
expect(jumpClient.end).toHaveBeenCalledOnce();
vi.useRealTimers();
});
});
+27
View File
@@ -1,4 +1,5 @@
import type { LogEntry, ConnectionStage } from "../../types/connection-log.js"; import type { LogEntry, ConnectionStage } from "../../types/connection-log.js";
import type { Client } from "ssh2";
export type StatsCapableHost = { export type StatsCapableHost = {
connectionType?: string; connectionType?: string;
@@ -21,6 +22,32 @@ export function isTcpPingEnabled(statsConfig: TcpPingStatsConfig): boolean {
return statsConfig.statusCheckEnabled && !statsConfig.disableTcpPing; return statsConfig.statusCheckEnabled && !statsConfig.disableTcpPing;
} }
export function tcpPingThroughJumpHost(
jumpClient: Pick<Client, "forwardOut" | "end">,
host: string,
port: number,
timeoutMs = 5000,
): Promise<boolean> {
return new Promise((resolve) => {
let settled = false;
const finish = (result: boolean) => {
if (settled) return;
settled = true;
clearTimeout(timeout);
jumpClient.end();
resolve(result);
};
const timeout = setTimeout(() => finish(false), timeoutMs);
jumpClient.forwardOut("127.0.0.1", 0, host, port, (error, stream) => {
stream?.destroy();
finish(!error && !!stream);
});
});
}
export function createConnectionLog( export function createConnectionLog(
type: "info" | "success" | "warning" | "error", type: "info" | "success" | "warning" | "error",
stage: ConnectionStage, stage: ConnectionStage,
+30 -8
View File
@@ -49,6 +49,7 @@ import {
createConnectionLog, createConnectionLog,
isTcpPingEnabled, isTcpPingEnabled,
supportsMetrics, supportsMetrics,
tcpPingThroughJumpHost,
} from "./host-metrics-helpers.js"; } from "./host-metrics-helpers.js";
import { applyAgentAuth } from "./terminal-auth-helpers.js"; import { applyAgentAuth } from "./terminal-auth-helpers.js";
import { import {
@@ -361,24 +362,45 @@ class PollingManager {
} }
try { try {
let pingHost = refreshedHost.ip;
const ct = refreshedHost.connectionType || "ssh"; const ct = refreshedHost.connectionType || "ssh";
let pingPort: number; let pingPort: number;
if (ct === "rdp") pingPort = refreshedHost.rdpPort ?? 3389; if (ct === "rdp") pingPort = refreshedHost.rdpPort ?? 3389;
else if (ct === "vnc") pingPort = refreshedHost.vncPort ?? 5900; else if (ct === "vnc") pingPort = refreshedHost.vncPort ?? 5900;
else if (ct === "telnet") pingPort = refreshedHost.telnetPort ?? 23; else if (ct === "telnet") pingPort = refreshedHost.telnetPort ?? 23;
else pingPort = refreshedHost.port; else pingPort = refreshedHost.port;
let isOnline: boolean;
if (refreshedHost.jumpHosts && refreshedHost.jumpHosts.length > 0) { if (refreshedHost.jumpHosts && refreshedHost.jumpHosts.length > 0) {
const firstJump = await fetchHostById( const proxyConfig: SOCKS5Config | null =
refreshedHost.jumpHosts[0].hostId, refreshedHost.useSocks5 &&
(refreshedHost.socks5Host ||
(refreshedHost.socks5ProxyChain &&
refreshedHost.socks5ProxyChain.length > 0))
? {
useSocks5: true,
socks5Host: refreshedHost.socks5Host,
socks5Port: refreshedHost.socks5Port,
socks5Username: refreshedHost.socks5Username,
socks5Password: refreshedHost.socks5Password,
socks5ProxyChain: refreshedHost.socks5ProxyChain,
}
: null;
const jumpClient = await createJumpHostChain(
refreshedHost.jumpHosts,
userId, userId,
proxyConfig,
); );
if (firstJump) { isOnline = jumpClient
pingHost = firstJump.ip; ? await tcpPingThroughJumpHost(
pingPort = firstJump.port; jumpClient,
} refreshedHost.ip,
pingPort,
5000,
)
: false;
} else {
isOnline = await tcpPing(refreshedHost.ip, pingPort, 5000);
} }
const isOnline = await tcpPing(pingHost, pingPort, 5000);
const statusEntry: StatusEntry = { const statusEntry: StatusEntry = {
status: isOnline ? "online" : "offline", status: isOnline ? "online" : "offline",
lastChecked: new Date().toISOString(), lastChecked: new Date().toISOString(),