round46: Prozessweite Verbindungs-Obergrenze statt zwei sich multiplizierender Parallelitaets-Werte (behebt verlorene Ports wie FritzBox 443 bei mehreren gleichzeitigen Geraete-Scans)

This commit is contained in:
2026-07-25 13:56:48 +02:00
parent 6792f5b7ea
commit eed5089a56

View File

@@ -17,12 +17,58 @@ export const TYPICAL_PORTS = [
8181, 8282,
];
/**
* Einfacher Zähl-Semaphore, um die GESAMTZAHL gleichzeitig offener Sockets
* über den kompletten Backend-Prozess hinweg zu begrenzen - nicht nur pro
* einzelnem Geräte-Scan. Wichtig, seit mehrere Geräte gleichzeitig gescannt
* werden (siehe routes/scan.ts, CONCURRENT_DEVICES): ohne einen
* PROZESSWEITEN Deckel hätten sich Geräte-Parallelität (bis zu 10) und
* Port-Batch-Größe (bis zu 1000) multipliziert - bis zu 10.000 gleichzeitige
* Verbindungsversuche, die den Container/das Heimnetz überfordern und dabei
* auch eigentlich funktionierende Ports (wie 443 der FritzBox selbst)
* fehlschlagen lassen konnten.
*/
class Semaphore {
private available: number;
private readonly queue: (() => void)[] = [];
constructor(count: number) {
this.available = count;
}
acquire(): Promise<void> {
if (this.available > 0) {
this.available--;
return Promise.resolve();
}
return new Promise((resolve) => this.queue.push(resolve));
}
release(): void {
const next = this.queue.shift();
if (next) {
next();
} else {
this.available++;
}
}
}
// Prozessweiter Deckel für gleichzeitig offene Scan-Sockets - unabhängig
// davon, ob das gerade ein einzelner Geräte-Scan oder mehrere parallele
// Geräte-Scans sind (siehe Kommentar oben).
const globalScanSemaphore = new Semaphore(300);
/**
* Prüft per TCP-Connect, ob ein Port offen ist. Kein Protokoll-Handshake,
* nur "kann eine Verbindung aufgebaut werden" schnell und protokollunabhängig.
* Wartet ggf. kurz auf ein freies "Ticket" im prozessweiten Semaphore (siehe
* oben), bevor der eigentliche Verbindungsversuch losgeht.
*/
export function isPortOpen(host: string, port: number, timeoutMs = 800): Promise<boolean> {
return new Promise((resolve) => {
export async function isPortOpen(host: string, port: number, timeoutMs = 800): Promise<boolean> {
await globalScanSemaphore.acquire();
try {
return await new Promise((resolve) => {
const socket = connect({ host, port, timeout: timeoutMs });
const finish = (result: boolean) => {
@@ -35,14 +81,18 @@ export function isPortOpen(host: string, port: number, timeoutMs = 800): Promise
socket.once("timeout", () => finish(false));
socket.once("error", () => finish(false));
});
} finally {
globalScanSemaphore.release();
}
}
/**
* Prüft eine (ggf. sehr lange) Liste von Ports in Batches mit begrenzter
* Parallelität - für den vollständigen Portscan (1-65535, siehe
* scanDeviceServices mit fullScan=true). Alle Ports auf einmal parallel zu
* prüfen wäre bei 65535 Ports zu viel gleichzeitig offener Sockets; komplett
* nacheinander wäre selbst bei schnellen RSTs zu langsam.
* Prüft eine (ggf. sehr lange) Liste von Ports - für den vollständigen
* Portscan (1-65535, siehe scanDeviceServices mit fullScan=true). Alle
* Verbindungsversuche werden sofort gestartet, die eigentliche Begrenzung
* der Gleichzeitigkeit übernimmt der prozessweite Semaphore in isPortOpen
* (siehe oben) - dadurch bleibt die Gesamtzahl gleichzeitiger Sockets sicher
* gedeckelt, auch wenn mehrere Geräte gleichzeitig gescannt werden.
*
* Timeout bewusst knapp, aber nicht zu knapp (500ms): im lokalen Netz
* (derselbe Subnetz-Bereich) liegt die Round-Trip-Zeit normalerweise im
@@ -55,18 +105,12 @@ export function isPortOpen(host: string, port: number, timeoutMs = 800): Promise
export async function scanPortsInBatches(
host: string,
ports: number[],
batchSize = 1000,
timeoutMs = 500
): Promise<number[]> {
const open: number[] = [];
for (let i = 0; i < ports.length; i += batchSize) {
const batch = ports.slice(i, i + batchSize);
const results = await Promise.all(
batch.map(async (port) => ({ port, open: await isPortOpen(host, port, timeoutMs) }))
ports.map(async (port) => ({ port, open: await isPortOpen(host, port, timeoutMs) }))
);
for (const r of results) if (r.open) open.push(r.port);
}
return open;
return results.filter((r) => r.open).map((r) => r.port);
}
/** Alle Ports von 1 bis 65535 - für den vollständigen (statt nur "typische Ports") Scan eines einzelnen Geräts. */