mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
fix(status): skip TCP probes while host sessions are active (#1538)
This commit is contained in:
1 parent
bd0858b5a2
commit
dc5d57c012
6 files changed
+50
-8
No files matched your search
@@ -11,6 +11,10 @@ export class HostSessionStatus {
|
|||||||
private counts = new Map<number, number>();
|
private counts = new Map<number, number>();
|
||||||
private listeners = new Set<HostSessionStatusListener>();
|
private listeners = new Set<HostSessionStatusListener>();
|
||||||
|
|
||||||
|
hasActiveSession(hostId: number): boolean {
|
||||||
|
return (this.counts.get(hostId) ?? 0) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
register(hostId: number): () => void {
|
register(hostId: number): () => void {
|
||||||
const count = this.counts.get(hostId) ?? 0;
|
const count = this.counts.get(hostId) ?? 0;
|
||||||
this.counts.set(hostId, count + 1);
|
this.counts.set(hostId, count + 1);
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export interface HostStatusDeps {
|
|||||||
target: StatusTarget,
|
target: StatusTarget,
|
||||||
port: number,
|
port: number,
|
||||||
) => Promise<boolean>;
|
) => Promise<boolean>;
|
||||||
|
hasActiveSession?: (hostId: number) => boolean;
|
||||||
globalInterval: () => number;
|
globalInterval: () => number;
|
||||||
emit: (payload: HostStatusPayload) => void;
|
emit: (payload: HostStatusPayload) => void;
|
||||||
}
|
}
|
||||||
@@ -126,6 +127,7 @@ const defaultDeps: HostStatusDeps = {
|
|||||||
);
|
);
|
||||||
return [...new Set(entries.map((entry) => entry.hostId))];
|
return [...new Set(entries.map((entry) => entry.hostId))];
|
||||||
},
|
},
|
||||||
|
hasActiveSession: (hostId) => hostSessionStatus.hasActiveSession(hostId),
|
||||||
ping: (host, port) => tcpPing(host, port, 5000),
|
ping: (host, port) => tcpPing(host, port, 5000),
|
||||||
pingThroughJumpHosts: async (target, port) => {
|
pingThroughJumpHosts: async (target, port) => {
|
||||||
const { createJumpHostChain } = await import("../jump-host-chain.js");
|
const { createJumpHostChain } = await import("../jump-host-chain.js");
|
||||||
@@ -429,11 +431,15 @@ export class HostStatusService {
|
|||||||
this.owners.set(target.id, target.userId);
|
this.owners.set(target.id, target.userId);
|
||||||
let reachable = false;
|
let reachable = false;
|
||||||
try {
|
try {
|
||||||
const port = await this.portFor(target);
|
if (this.deps.hasActiveSession?.(target.id)) {
|
||||||
reachable =
|
reachable = true;
|
||||||
target.jumpHosts.length > 0
|
} else {
|
||||||
? await this.deps.pingThroughJumpHosts(target, port)
|
const port = await this.portFor(target);
|
||||||
: await this.deps.ping(target.ip, port);
|
reachable =
|
||||||
|
target.jumpHosts.length > 0
|
||||||
|
? await this.deps.pingThroughJumpHosts(target, port)
|
||||||
|
: await this.deps.ping(target.ip, port);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
reachable = false;
|
reachable = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import net from "net";
|
|||||||
import type { Client } from "ssh2";
|
import type { Client } from "ssh2";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a TCP connection and closes it again. An SSH server gets a polite
|
* Opens a TCP connection and closes it again. SSH probes exchange banners,
|
||||||
* banner back so it does not log a failed handshake.
|
* but still close before authentication and may trigger aggressive Fail2Ban rules.
|
||||||
*/
|
*/
|
||||||
export function tcpPing(
|
export function tcpPing(
|
||||||
host: string,
|
host: string,
|
||||||
|
|||||||
@@ -13,10 +13,13 @@ describe("HostSessionStatus", () => {
|
|||||||
expect(listener).toHaveBeenCalledTimes(1);
|
expect(listener).toHaveBeenCalledTimes(1);
|
||||||
expect(listener).toHaveBeenLastCalledWith(7, true);
|
expect(listener).toHaveBeenLastCalledWith(7, true);
|
||||||
|
|
||||||
|
expect(status.hasActiveSession(7)).toBe(true);
|
||||||
closeFirst();
|
closeFirst();
|
||||||
|
expect(status.hasActiveSession(7)).toBe(true);
|
||||||
expect(listener).toHaveBeenCalledTimes(1);
|
expect(listener).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
closeSecond();
|
closeSecond();
|
||||||
|
expect(status.hasActiveSession(7)).toBe(false);
|
||||||
expect(listener).toHaveBeenLastCalledWith(7, false);
|
expect(listener).toHaveBeenLastCalledWith(7, false);
|
||||||
expect(listener).toHaveBeenCalledTimes(2);
|
expect(listener).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ function setup(
|
|||||||
loadSharedHostIds: async (userId) => shared[userId] ?? [],
|
loadSharedHostIds: async (userId) => shared[userId] ?? [],
|
||||||
ping,
|
ping,
|
||||||
pingThroughJumpHosts,
|
pingThroughJumpHosts,
|
||||||
|
hasActiveSession: (id) => hostSessionStatus.hasActiveSession(id),
|
||||||
globalInterval: () => 60,
|
globalInterval: () => 60,
|
||||||
emit: (payload) => emitted.push(payload),
|
emit: (payload) => emitted.push(payload),
|
||||||
});
|
});
|
||||||
@@ -74,6 +75,34 @@ afterEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("HostStatusService", () => {
|
describe("HostStatusService", () => {
|
||||||
|
it.each([{ jumpHosts: [] }, { jumpHosts: [{ hostId: 9 }] }])(
|
||||||
|
"skips probes while a session is open, including jump hosts: %j",
|
||||||
|
async ({ jumpHosts }) => {
|
||||||
|
const { service, ping, pingThroughJumpHosts } = setup([
|
||||||
|
target(7, { jumpHosts }),
|
||||||
|
]);
|
||||||
|
active = service;
|
||||||
|
const close = hostSessionStatus.register(7);
|
||||||
|
try {
|
||||||
|
await service.statusesFor("owner", null);
|
||||||
|
await flush();
|
||||||
|
await vi.advanceTimersByTimeAsync(60_000);
|
||||||
|
await flush();
|
||||||
|
expect(service.get(7)?.status).toBe("online");
|
||||||
|
expect(ping).not.toHaveBeenCalled();
|
||||||
|
expect(pingThroughJumpHosts).not.toHaveBeenCalled();
|
||||||
|
close();
|
||||||
|
await vi.advanceTimersByTimeAsync(60_000);
|
||||||
|
await flush();
|
||||||
|
expect(
|
||||||
|
jumpHosts.length ? pingThroughJumpHosts : ping,
|
||||||
|
).toHaveBeenCalledOnce();
|
||||||
|
} finally {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("starts a user's own hosts and reports them online", async () => {
|
it("starts a user's own hosts and reports them online", async () => {
|
||||||
const { service, emitted, ping } = setup([target(1), target(2)]);
|
const { service, emitted, ping } = setup([target(1), target(2)]);
|
||||||
active = service;
|
active = service;
|
||||||
|
|||||||
@@ -479,7 +479,7 @@
|
|||||||
"sameHost": "This host (direct tunnel)",
|
"sameHost": "This host (direct tunnel)",
|
||||||
"statusChecksLabel": "Status Checks",
|
"statusChecksLabel": "Status Checks",
|
||||||
"enableStatusChecks": "Enable Status Checks",
|
"enableStatusChecks": "Enable Status Checks",
|
||||||
"enableStatusChecksDesc": "Periodically ping this host to verify availability",
|
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||||
"useGlobalInterval": "Use Global Interval",
|
"useGlobalInterval": "Use Global Interval",
|
||||||
"useGlobalIntervalDesc": "Override with the server-wide status check interval",
|
"useGlobalIntervalDesc": "Override with the server-wide status check interval",
|
||||||
"checkIntervalS": "Check Interval (s)",
|
"checkIntervalS": "Check Interval (s)",
|
||||||
|
|||||||
Reference in new issue
Block a user