generated from Dicken/dickendock
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:
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user