generated from Dicken/dickendock
round50: masscan/httpx laufen im Sammel-Scan nur noch einmal fuer alle Geraete statt pro Geraet, Rate deutlich erhoeht
This commit is contained in:
@@ -4,8 +4,10 @@ import * as serviceRepo from "../db/repositories/services.js";
|
|||||||
import * as categoryRepo from "../db/repositories/categories.js";
|
import * as categoryRepo from "../db/repositories/categories.js";
|
||||||
import * as logRepo from "../db/repositories/logs.js";
|
import * as logRepo from "../db/repositories/logs.js";
|
||||||
import * as settingsRepo from "../db/repositories/settings.js";
|
import * as settingsRepo from "../db/repositories/settings.js";
|
||||||
import { scanDeviceServices } from "../scanner/networkScanner.js";
|
import { scanDeviceServices, type PreFetchedScanData } from "../scanner/networkScanner.js";
|
||||||
import { fullPortRange } from "../scanner/ports.js";
|
import { fullPortRange } from "../scanner/ports.js";
|
||||||
|
import { scanHostsWithMasscan, getMasscanLastError } from "../scanner/masscan.js";
|
||||||
|
import { probeManyWithHttpx, getHttpxLastError, type HttpxResult } from "../scanner/httpx.js";
|
||||||
import { fetchFritzBoxHosts, type FritzBoxHost } from "../scanner/fritzbox.js";
|
import { fetchFritzBoxHosts, type FritzBoxHost } from "../scanner/fritzbox.js";
|
||||||
import {
|
import {
|
||||||
startJob,
|
startJob,
|
||||||
@@ -54,7 +56,7 @@ function formatToolsUsed(toolsUsed: {
|
|||||||
* Einzelgerät-Endpunkt als auch der Sammel-Scan (siehe /api/scan/devices/all
|
* Einzelgerät-Endpunkt als auch der Sammel-Scan (siehe /api/scan/devices/all
|
||||||
* weiter unten) dieselbe Logik nutzen, ohne sie zu duplizieren.
|
* weiter unten) dieselbe Logik nutzen, ohne sie zu duplizieren.
|
||||||
*/
|
*/
|
||||||
async function performDeviceScan(device: Device, fullScan = false) {
|
async function performDeviceScan(device: Device, fullScan = false, preFetched?: PreFetchedScanData) {
|
||||||
// Bestehende Dienste dieses Geräts VOR dem Scan merken, um danach zu
|
// Bestehende Dienste dieses Geräts VOR dem Scan merken, um danach zu
|
||||||
// erkennen, welche davon diesmal nicht mehr gefunden wurden ("stale").
|
// erkennen, welche davon diesmal nicht mehr gefunden wurden ("stale").
|
||||||
const servicesBeforeScan = serviceRepo.listServicesByDevice(device.id);
|
const servicesBeforeScan = serviceRepo.listServicesByDevice(device.id);
|
||||||
@@ -62,7 +64,8 @@ async function performDeviceScan(device: Device, fullScan = false) {
|
|||||||
const { services: discovered, suggestedHostname, toolsUsed } = await scanDeviceServices(
|
const { services: discovered, suggestedHostname, toolsUsed } = await scanDeviceServices(
|
||||||
device,
|
device,
|
||||||
fullScan ? fullPortRange() : undefined,
|
fullScan ? fullPortRange() : undefined,
|
||||||
servicesBeforeScan.map((s) => s.port)
|
servicesBeforeScan.map((s) => s.port),
|
||||||
|
preFetched
|
||||||
);
|
);
|
||||||
|
|
||||||
// Jede erkannte Kategorie auch in der categories-Tabelle anlegen, damit
|
// Jede erkannte Kategorie auch in der categories-Tabelle anlegen, damit
|
||||||
@@ -230,10 +233,48 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// NEU (round50): Bis round49 startete JEDES Gerät seinen eigenen
|
||||||
|
// masscan- bzw. httpx-Prozess - bei bis zu 10 gleichzeitig
|
||||||
|
// gescannten Geräten (siehe CONCURRENT_DEVICES unten) liefen dadurch
|
||||||
|
// bis zu 10 masscan- und 10 httpx-Prozesse parallel, die sich
|
||||||
|
// gegenseitig die Netzwerkschnittstelle streitig machten und in den
|
||||||
|
// Timeout liefen (siehe Bugreport: "Flaschenhals").
|
||||||
|
//
|
||||||
|
// Jetzt läuft masscan EINMAL für ALLE Geräte dieses Scan-Laufs
|
||||||
|
// gemeinsam (masscan unterstützt mehrere Ziel-IPs nativ in einem
|
||||||
|
// Prozess) und httpx EINMAL für die Vereinigung aller dabei
|
||||||
|
// gefundenen offenen Ports über alle Geräte hinweg. Das Ergebnis
|
||||||
|
// wird unten pro Gerät als "preFetched" an performDeviceScan
|
||||||
|
// durchgereicht - der eigentliche Geräte-Scan macht dann selbst
|
||||||
|
// keine masscan-/httpx-Aufrufe mehr, nur noch die (leichtgewichtigen,
|
||||||
|
// reinen HTTP-)Detailabfragen pro Port (Titel, Favicon,
|
||||||
|
// Softwareerkennung).
|
||||||
|
const deviceIps = devices.map((d) => d.ip);
|
||||||
|
const masscanBatch = await scanHostsWithMasscan(deviceIps, "1-65535");
|
||||||
|
const masscanBatchError = masscanBatch === null ? getMasscanLastError() : null;
|
||||||
|
|
||||||
|
// httpx nur für die Geräte vorab bündeln, für die masscan Ergebnisse
|
||||||
|
// geliefert hat - ist masscan für den GESAMTEN Batch fehlgeschlagen
|
||||||
|
// (masscanBatch === null), kennen wir die offenen Ports noch nicht
|
||||||
|
// und jedes Gerät ermittelt sie unten wie gewohnt selbst per
|
||||||
|
// TCP-Fallback (scanPortsInBatches) samt eigenem httpx-Aufruf -
|
||||||
|
// langsamer als der Idealfall, aber weiterhin korrekt.
|
||||||
|
let httpxBatch: Map<string, HttpxResult> | null = null;
|
||||||
|
let httpxBatchError: string | null = null;
|
||||||
|
if (masscanBatch) {
|
||||||
|
const httpxTargets: Array<{ ip: string; port: number }> = [];
|
||||||
|
for (const device of devices) {
|
||||||
|
const ports = masscanBatch.get(device.ip) ?? [];
|
||||||
|
for (const port of ports) httpxTargets.push({ ip: device.ip, port });
|
||||||
|
}
|
||||||
|
httpxBatch = await probeManyWithHttpx(httpxTargets);
|
||||||
|
if (httpxBatch === null) httpxBatchError = getHttpxLastError();
|
||||||
|
}
|
||||||
|
|
||||||
// Mehrere Geräte GLEICHZEITIG scannen statt strikt nacheinander -
|
// Mehrere Geräte GLEICHZEITIG scannen statt strikt nacheinander -
|
||||||
// bei z. B. 118 Geräten und je 10-30 Sekunden pro vollständigem
|
// bei z. B. 118 Geräten hätte ein rein sequenzieller Durchlauf der
|
||||||
// Portscan hätte ein rein sequenzieller Durchlauf zusammen leicht
|
// (jetzt viel schnelleren, weil rein HTTP-basierten) Detailabfragen
|
||||||
// 30+ Minuten gedauert. Batch-Größe bewusst moderat (nicht alle
|
// trotzdem lange gedauert. Batch-Größe bewusst moderat (nicht alle
|
||||||
// Geräte auf einmal): jeder einzelne Geräte-Scan öffnet intern schon
|
// Geräte auf einmal): jeder einzelne Geräte-Scan öffnet intern schon
|
||||||
// bis zu 1000 gleichzeitige Verbindungen (siehe ports.ts) - zu viele
|
// bis zu 1000 gleichzeitige Verbindungen (siehe ports.ts) - zu viele
|
||||||
// Geräte gleichzeitig könnte das eigene Netz/den Docker-Host
|
// Geräte gleichzeitig könnte das eigene Netz/den Docker-Host
|
||||||
@@ -258,7 +299,28 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await performDeviceScan(device, true);
|
const masscanPorts = masscanBatch?.get(device.ip);
|
||||||
|
const preFetched: PreFetchedScanData = {
|
||||||
|
masscanAttempted: true,
|
||||||
|
masscanPorts,
|
||||||
|
masscanError: masscanPorts ? null : masscanBatchError,
|
||||||
|
httpxResults: masscanPorts
|
||||||
|
? (() => {
|
||||||
|
// Aus der großen, geräteübergreifenden httpx-Map nur
|
||||||
|
// die Treffer DIESES Geräts herausfiltern.
|
||||||
|
if (httpxBatch === null) return null;
|
||||||
|
const map = new Map<number, HttpxResult>();
|
||||||
|
for (const port of masscanPorts) {
|
||||||
|
const hit = httpxBatch.get(`${device.ip}:${port}`);
|
||||||
|
if (hit) map.set(port, hit);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
})()
|
||||||
|
: undefined,
|
||||||
|
httpxError: masscanPorts ? httpxBatchError : null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await performDeviceScan(device, true, preFetched);
|
||||||
created += result.created;
|
created += result.created;
|
||||||
updated += result.updated;
|
updated += result.updated;
|
||||||
allStale.push(...result.staleServices);
|
allStale.push(...result.staleServices);
|
||||||
|
|||||||
@@ -24,16 +24,12 @@ export function getHttpxLastError(): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Einfacher Zähl-Semaphore (siehe auch masscan.ts), hier mit Kapazität 3:
|
* Einfacher Zähl-Semaphore (siehe auch masscan.ts), Kapazität 2: Seit
|
||||||
* Beim Sammel-Scan laufen bis zu 10 Geräte-Scans gleichzeitig (siehe
|
* round50 läuft pro Scan-JOB nur noch EIN httpx-Prozess für ALLE Geräte
|
||||||
* routes/scan.ts CONCURRENT_DEVICES), jeder startet einen eigenen
|
* gleichzeitig (siehe probeManyWithHttpx unten), nicht mehr einer PRO Gerät
|
||||||
* httpx-Prozess mit intern 50 Threads - ohne Deckel also bis zu 500
|
* wie bis round49. Der Semaphore schützt nur noch den seltenen Fall, dass
|
||||||
* gleichzeitige HTTP-Anfragen ALLEIN durch httpx, zusätzlich zum eigenen
|
* zwei unabhängige Scan-Jobs (Sammel-Scan + gleichzeitig ein manueller
|
||||||
* TCP-Verbindungs-Semaphore (ports.ts). Unter dieser Last liefen einzelne
|
* Einzelgeräte-Scan) überlappen.
|
||||||
* httpx-Läufe in den 5s-Timeout pro Port und damit leer, obwohl der Dienst
|
|
||||||
* eigentlich erreichbar war - der Port verschwand dann komplett aus dem
|
|
||||||
* Ergebnis (siehe networkScanner.ts). Kapazität 3 begrenzt das auf maximal
|
|
||||||
* 150 gleichzeitige httpx-Anfragen prozessweit.
|
|
||||||
*/
|
*/
|
||||||
class Semaphore {
|
class Semaphore {
|
||||||
private available: number;
|
private available: number;
|
||||||
@@ -58,33 +54,69 @@ class Semaphore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const httpxSemaphore = new Semaphore(3);
|
const httpxSemaphore = new Semaphore(2);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Threads für den httpx-Aufruf: skaliert leicht mit der Zielanzahl (mehr
|
||||||
|
* Ziele -> mehr Parallelität sinnvoll), aber mit Ober-/Untergrenze. Seit nur
|
||||||
|
* noch EIN httpx-Prozess pro Scan-Job läuft (statt bis zu 10 gleichzeitig),
|
||||||
|
* ist ein höherer Wert als die bisherigen festen 50 unproblematisch - dieser
|
||||||
|
* eine Prozess hat die volle Bandbreite für sich.
|
||||||
|
*/
|
||||||
|
function computeThreads(targetCount: number): number {
|
||||||
|
return Math.min(200, Math.max(50, targetCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schätzt einen angemessenen Gesamt-Timeout für einen httpx-Lauf über
|
||||||
|
* mehrere Ziele: (Zielanzahl / Threads) * Zeit pro Versuch (Timeout *
|
||||||
|
* (1 + Retries)), mit Sicherheitsfaktor und Grenzen (30s..10min).
|
||||||
|
*/
|
||||||
|
function computeTimeoutMs(targetCount: number, threads: number, perProbeTimeoutSec: number, retries: number): number {
|
||||||
|
const perProbeSeconds = perProbeTimeoutSec * (1 + retries);
|
||||||
|
const estimatedSeconds = (targetCount / threads) * perProbeSeconds;
|
||||||
|
const withMargin = estimatedSeconds * 1.5 + 10;
|
||||||
|
return Math.min(Math.max(withMargin * 1000, 30_000), 10 * 60_000);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nutzt httpx (ProjectDiscovery, falls im Container installiert - siehe
|
* Nutzt httpx (ProjectDiscovery, falls im Container installiert - siehe
|
||||||
* Dockerfile) um von einer Liste offener Ports (z. B. das Ergebnis von
|
* Dockerfile), um eine LISTE von "ip:port"-Zielen ÜBER MEHRERE GERÄTE HINWEG
|
||||||
* masscan) NUR die herauszufiltern, die tatsächlich HTTP oder HTTPS
|
* in einem einzigen Prozesslauf zu prüfen, statt pro Gerät einen eigenen
|
||||||
* sprechen - inklusive Status-Code/Titel/Server-Header in einem einzigen,
|
* httpx-Prozess zu starten (das führte bei vielen parallel gescannten
|
||||||
* hochparallelen Durchlauf. Deutlich schneller und präziser als unser
|
* Geräten - siehe routes/scan.ts CONCURRENT_DEVICES - zu bis zu 500
|
||||||
* eigenes Protokoll-Raten (erst HTTP versuchen, bei Fehlschlag HTTPS
|
* gleichzeitigen HTTP-Anfragen allein durch httpx und damit zu Timeouts
|
||||||
* versuchen) für JEDEN einzelnen offenen Port einzeln.
|
* unter Last, siehe Bugreport).
|
||||||
*
|
*
|
||||||
* Liefert null (nicht einen leeren Array!), wenn httpx fehlt oder
|
* Ergebnis ist eine Map, deren Schlüssel "ip:port" ist (z. B.
|
||||||
|
* "192.168.1.50:8080") - zusammengesetzt aus httpx' eigenem "url"-Feld
|
||||||
|
* (IMMER Teil der -json-Ausgabe, im Gegensatz zu ggf. je nach Version
|
||||||
|
* abweichenden Feldern wie "input"), damit die Zuordnung robust gegenüber
|
||||||
|
* kleineren httpx-Versionsunterschieden ist.
|
||||||
|
*
|
||||||
|
* Liefert null (nicht eine leere Map!), wenn httpx fehlt oder komplett
|
||||||
* fehlschlägt - der Aufrufer erkennt daran "kein httpx verfügbar" und fällt
|
* fehlschlägt - der Aufrufer erkennt daran "kein httpx verfügbar" und fällt
|
||||||
* auf die eingebaute Methode zurück, statt fälschlich "keine Webdienste
|
* für ALLE betroffenen Ports auf die eingebaute Methode zurück. Ein Ziel,
|
||||||
* gefunden" anzunehmen. Wichtig: httpx meldet für einen Port, den es nicht
|
* das httpx innerhalb des Timeouts nicht erreicht, taucht in der Map
|
||||||
* innerhalb des Timeouts erreicht, GAR NICHTS - das ist von außen nicht von
|
* schlicht nicht auf - von hier aus nicht unterscheidbar von "spricht kein
|
||||||
* "das ist wirklich kein Webdienst" zu unterscheiden. networkScanner.ts
|
* HTTP". Die Aufrufer (networkScanner.ts) prüfen daher jeden offenen Port,
|
||||||
* behandelt daher von httpx nicht bestätigte, aber offene Ports weiterhin
|
* der NICHT in der Map auftaucht, sicherheitshalber trotzdem noch einmal
|
||||||
* mit einer eigenen Nachprüfung, statt sie stillschweigend zu verwerfen.
|
* selbst nach, statt ihn zu verwerfen.
|
||||||
*/
|
*/
|
||||||
export async function probeWithHttpx(ip: string, ports: number[]): Promise<Map<number, HttpxResult> | null> {
|
export async function probeManyWithHttpx(
|
||||||
if (httpxConfirmedUnavailable || ports.length === 0) {
|
targets: Array<{ ip: string; port: number }>
|
||||||
return ports.length === 0 ? new Map() : null;
|
): Promise<Map<string, HttpxResult> | null> {
|
||||||
|
if (httpxConfirmedUnavailable || targets.length === 0) {
|
||||||
|
return targets.length === 0 ? new Map() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
await httpxSemaphore.acquire();
|
await httpxSemaphore.acquire();
|
||||||
try {
|
try {
|
||||||
|
const threads = computeThreads(targets.length);
|
||||||
|
const perProbeTimeoutSec = 8;
|
||||||
|
const retries = 1;
|
||||||
|
const timeoutMs = computeTimeoutMs(targets.length, threads, perProbeTimeoutSec, retries);
|
||||||
|
|
||||||
return await new Promise((resolve) => {
|
return await new Promise((resolve) => {
|
||||||
const child = spawn(
|
const child = spawn(
|
||||||
"httpx",
|
"httpx",
|
||||||
@@ -92,12 +124,12 @@ export async function probeWithHttpx(ip: string, ports: number[]): Promise<Map<n
|
|||||||
"-silent",
|
"-silent",
|
||||||
"-json",
|
"-json",
|
||||||
"-timeout",
|
"-timeout",
|
||||||
"8",
|
String(perProbeTimeoutSec),
|
||||||
"-retries",
|
"-retries",
|
||||||
"1",
|
String(retries),
|
||||||
"-no-color",
|
"-no-color",
|
||||||
"-threads",
|
"-threads",
|
||||||
"50",
|
String(threads),
|
||||||
],
|
],
|
||||||
{ stdio: ["pipe", "pipe", "pipe"] }
|
{ stdio: ["pipe", "pipe", "pipe"] }
|
||||||
);
|
);
|
||||||
@@ -106,7 +138,7 @@ export async function probeWithHttpx(ip: string, ports: number[]): Promise<Map<n
|
|||||||
let stderr = "";
|
let stderr = "";
|
||||||
let settled = false;
|
let settled = false;
|
||||||
|
|
||||||
const finish = (result: Map<number, HttpxResult> | null) => {
|
const finish = (result: Map<string, HttpxResult> | null) => {
|
||||||
if (settled) return;
|
if (settled) return;
|
||||||
settled = true;
|
settled = true;
|
||||||
resolve(result);
|
resolve(result);
|
||||||
@@ -129,25 +161,19 @@ export async function probeWithHttpx(ip: string, ports: number[]): Promise<Map<n
|
|||||||
stderr += chunk.toString("utf-8");
|
stderr += chunk.toString("utf-8");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Grober oberer Zeitrahmen für den GESAMTEN httpx-Lauf (alle Ports
|
|
||||||
// zusammen, dank -threads 50 weitgehend parallel) - unabhängig vom
|
|
||||||
// Pro-Port-Timeout oben. 60s statt vorheriger 30s, damit ein Gerät mit
|
|
||||||
// vielen offenen Ports (nach einem vollständigen 1-65535-Portscan)
|
|
||||||
// nicht schon bei normaler Auslastung abgebrochen wird.
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
httpxLastError = `Zeitüberschreitung (60s) - httpx wurde beendet, bevor alle ${ports.length} Port(s) geprüft waren`;
|
httpxLastError = `Zeitüberschreitung (${Math.round(timeoutMs / 1000)}s) - httpx wurde beendet, bevor alle ${targets.length} Ziel(e) geprüft waren`;
|
||||||
child.kill();
|
child.kill();
|
||||||
finish(null);
|
finish(null);
|
||||||
}, 60_000);
|
}, timeoutMs);
|
||||||
|
|
||||||
child.on("close", (code) => {
|
child.on("close", (code) => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
// War der Lauf schon durch den Timeout oben als fehlgeschlagen
|
// War der Lauf schon durch den Timeout oben als fehlgeschlagen
|
||||||
// markiert (settled=true), NICHT mehr überschreiben - sonst geht die
|
// markiert (settled=true), NICHT mehr überschreiben - sonst geht die
|
||||||
// konkrete "Zeitüberschreitung"-Meldung unter der generischen
|
// konkrete "Zeitüberschreitung"-Meldung unter der generischen
|
||||||
// Exit-Code-Meldung verloren, die "close" hier sonst noch setzen
|
// Exit-Code-Meldung unter, die "close" hier sonst noch setzen würde
|
||||||
// würde (der Kill selbst löst "close" mit code=null erst danach
|
// (der Kill selbst löst "close" erst danach aus).
|
||||||
// aus).
|
|
||||||
if (settled) return;
|
if (settled) return;
|
||||||
|
|
||||||
if (code !== 0 && stdout.trim().length === 0) {
|
if (code !== 0 && stdout.trim().length === 0) {
|
||||||
@@ -159,12 +185,13 @@ export async function probeWithHttpx(ip: string, ports: number[]): Promise<Map<n
|
|||||||
|
|
||||||
httpxLastError = null; // erfolgreicher Lauf - alten Fehler nicht mehr anzeigen
|
httpxLastError = null; // erfolgreicher Lauf - alten Fehler nicht mehr anzeigen
|
||||||
|
|
||||||
const results = new Map<number, HttpxResult>();
|
const results = new Map<string, HttpxResult>();
|
||||||
for (const line of stdout.split("\n")) {
|
for (const line of stdout.split("\n")) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
if (!trimmed) continue;
|
if (!trimmed) continue;
|
||||||
try {
|
try {
|
||||||
const obj = JSON.parse(trimmed) as {
|
const obj = JSON.parse(trimmed) as {
|
||||||
|
url?: string;
|
||||||
port?: string;
|
port?: string;
|
||||||
scheme?: string;
|
scheme?: string;
|
||||||
status_code?: number;
|
status_code?: number;
|
||||||
@@ -172,8 +199,20 @@ export async function probeWithHttpx(ip: string, ports: number[]): Promise<Map<n
|
|||||||
webserver?: string;
|
webserver?: string;
|
||||||
};
|
};
|
||||||
const port = Number(obj.port);
|
const port = Number(obj.port);
|
||||||
if (!Number.isFinite(port)) continue;
|
if (!Number.isFinite(port) || !obj.url) continue;
|
||||||
results.set(port, {
|
|
||||||
|
// IP aus dem "url"-Feld ziehen (immer vorhanden, z. B.
|
||||||
|
// "https://192.168.1.50:8080") statt aus ggf. je nach
|
||||||
|
// httpx-Version abweichenden Zusatzfeldern - siehe Kommentar
|
||||||
|
// oben an der Funktion.
|
||||||
|
let ip: string;
|
||||||
|
try {
|
||||||
|
ip = new URL(obj.url).hostname;
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.set(`${ip}:${port}`, {
|
||||||
port,
|
port,
|
||||||
scheme: obj.scheme === "https" ? "https" : "http",
|
scheme: obj.scheme === "https" ? "https" : "http",
|
||||||
statusCode: obj.status_code,
|
statusCode: obj.status_code,
|
||||||
@@ -189,8 +228,8 @@ export async function probeWithHttpx(ip: string, ports: number[]): Promise<Map<n
|
|||||||
|
|
||||||
// Eingabe: "ip:port" pro Zeile, httpx erkennt Schema (http/https) je
|
// Eingabe: "ip:port" pro Zeile, httpx erkennt Schema (http/https) je
|
||||||
// Eintrag selbst (Standardverhalten: erst https versuchen, sonst http).
|
// Eintrag selbst (Standardverhalten: erst https versuchen, sonst http).
|
||||||
for (const port of ports) {
|
for (const t of targets) {
|
||||||
child.stdin.write(`${ip}:${port}\n`);
|
child.stdin.write(`${t.ip}:${t.port}\n`);
|
||||||
}
|
}
|
||||||
child.stdin.end();
|
child.stdin.end();
|
||||||
});
|
});
|
||||||
@@ -198,3 +237,19 @@ export async function probeWithHttpx(ip: string, ports: number[]): Promise<Map<n
|
|||||||
httpxSemaphore.release();
|
httpxSemaphore.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einzelgerät-Variante von probeManyWithHttpx() - für den "Jetzt scannen"-
|
||||||
|
* Button auf der Geräteseite (ein Gerät, kein Sammel-Scan).
|
||||||
|
*/
|
||||||
|
export async function probeWithHttpx(ip: string, ports: number[]): Promise<Map<number, HttpxResult> | null> {
|
||||||
|
const result = await probeManyWithHttpx(ports.map((port) => ({ ip, port })));
|
||||||
|
if (result === null) return null;
|
||||||
|
|
||||||
|
const map = new Map<number, HttpxResult>();
|
||||||
|
for (const port of ports) {
|
||||||
|
const hit = result.get(`${ip}:${port}`);
|
||||||
|
if (hit) map.set(port, hit);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ let masscanConfirmedUnavailable = false;
|
|||||||
// ist aber KEINE Fehlermeldung von masscan selbst, sondern der letzte Stand
|
// ist aber KEINE Fehlermeldung von masscan selbst, sondern der letzte Stand
|
||||||
// VOR dem Abbruch. Deshalb wird unten explizit geprüft, ob der Prozess per
|
// VOR dem Abbruch. Deshalb wird unten explizit geprüft, ob der Prozess per
|
||||||
// Timeout gekillt wurde (error.killed/error.signal), und das als eigener,
|
// Timeout gekillt wurde (error.killed/error.signal), und das als eigener,
|
||||||
// klar benannter Grund ausgegeben - statt die Restausgabe fälschlich als
|
// klar benannter Grund ausgegeben.
|
||||||
// "das ist der Fehler" zu präsentieren.
|
|
||||||
let masscanLastError: string | null = null;
|
let masscanLastError: string | null = null;
|
||||||
|
|
||||||
export function getMasscanLastError(): string | null {
|
export function getMasscanLastError(): string | null {
|
||||||
@@ -20,18 +19,19 @@ export function getMasscanLastError(): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Einfacher Zähl-Semaphore (Kopie des Patterns aus ports.ts), hier mit
|
* Einfacher Zähl-Semaphore mit Kapazität 1. Seit round49 lief HIERÜBER noch
|
||||||
* Kapazität 1: masscan verschickt SYN-Pakete per Rohsocket direkt über das
|
* pro GERÄT ein eigener masscan-Prozess - bei bis zu 10 parallel gescannten
|
||||||
* Netzwerk-Interface des Containers - laufen mehrere masscan-Prozesse
|
* Geräten (siehe routes/scan.ts CONCURRENT_DEVICES) wurden die vollständigen
|
||||||
* GLEICHZEITIG (z. B. weil der Sammel-Scan bis zu 10 Geräte parallel
|
* Portscans dadurch faktisch serialisiert und der Gesamtscan massiv
|
||||||
* abarbeitet, siehe routes/scan.ts CONCURRENT_DEVICES), konkurrieren sie um
|
* verlangsamt (siehe Bugreport). Seit round50 gibt es dafür
|
||||||
* dasselbe Interface. Das führt zu Paketverlust und dazu, dass einzelne
|
* scanHostsWithMasscan() weiter unten: EIN masscan-Prozess für ALLE Geräte
|
||||||
* Läufe den 90-Sekunden-Timeout reißen und fälschlich als "nicht
|
* eines Scan-Laufs gleichzeitig (masscan kann das nativ, siehe
|
||||||
* verfügbar" erscheinen, obwohl masscan grundsätzlich funktioniert -
|
* Mehrfach-Ziele als Argumente). Der Semaphore hier schützt nur noch den
|
||||||
* genau das beobachtete "mal geht's, mal nicht". Mit Kapazität 1 laufen
|
* seltenen Fall, dass zwei UNABHÄNGIGE Scan-Jobs (z. B. Sammel-Scan +
|
||||||
* masscan-Aufrufe aus mehreren parallelen Geräte-Scans strikt nacheinander,
|
* gleichzeitig ein manueller Einzelgeräte-Scan per Button) zufällig
|
||||||
* der Rest des jeweiligen Geräte-Scans (DNS, httpx, Softwareerkennung)
|
* überlappen - kostet dann fast nie Wartezeit, verhindert aber weiterhin,
|
||||||
* bleibt weiterhin parallel.
|
* dass zwei masscan-Prozesse sich dieselbe Netzwerkschnittstelle streitig
|
||||||
|
* machen.
|
||||||
*/
|
*/
|
||||||
class Semaphore {
|
class Semaphore {
|
||||||
private available: number;
|
private available: number;
|
||||||
@@ -58,41 +58,85 @@ class Semaphore {
|
|||||||
|
|
||||||
const masscanSemaphore = new Semaphore(1);
|
const masscanSemaphore = new Semaphore(1);
|
||||||
|
|
||||||
// masscan braucht bei rate=2000 für den vollen Portbereich (1-65535) rein
|
// Rate war bisher 2000 Pakete/Sekunde - ein sehr konservativer, für
|
||||||
// rechnerisch ca. 33s Sendezeit + einige Sekunden Nachlaufzeit für späte
|
// Internet-weite Scans gedachter Wert. Auf einem lokalen Heimnetz (kein
|
||||||
// Antworten. 60s waren in der Praxis (Docker-in-LXC, ggf. wartende
|
// WAN-Rauschen, keine Provider-Drosselung) verkraftet masscan problemlos
|
||||||
// Konkurrenz-Scans durch den neuen Semaphore oben) zu knapp bemessen und
|
// deutlich mehr, gerade jetzt wo nur noch EIN Prozess gleichzeitig läuft
|
||||||
// haben laufende, aber noch nicht fertige Scans abgewürgt. 90s geben Luft,
|
// (siehe Semaphore oben) statt sich mit bis zu 9 anderen die Bandbreite zu
|
||||||
// ohne einen wirklich hängenden Prozess ewig weiterlaufen zu lassen.
|
// teilen. 20.000 pps ist für masscan-Verhältnisse immer noch sehr moderat
|
||||||
const MASSCAN_TIMEOUT_MS = 90_000;
|
// (das Tool ist für Millionen pps ausgelegt), reicht aber, um z. B. 20
|
||||||
|
// Geräte à 65535 Ports in guten 60-90 Sekunden statt vielen Minuten
|
||||||
|
// abzudecken.
|
||||||
|
const DEFAULT_RATE = 20_000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zählt die Ports in einem masscan-Portbereich ("1-65535", "80,443,8000-8100"
|
||||||
|
* usw.) - für die Timeout-Schätzung unten. Fällt auf 65535 zurück, falls das
|
||||||
|
* Format nicht erkannt wird (sichere Obergrenze, lieber zu großzügig als zu
|
||||||
|
* knapp beim Timeout).
|
||||||
|
*/
|
||||||
|
function countPortsInRange(portRange: string): number {
|
||||||
|
let count = 0;
|
||||||
|
for (const part of portRange.split(",")) {
|
||||||
|
const [a, b] = part.split("-").map(Number);
|
||||||
|
if (Number.isFinite(a) && Number.isFinite(b)) count += Math.max(0, b - a + 1);
|
||||||
|
else if (Number.isFinite(a)) count += 1;
|
||||||
|
}
|
||||||
|
return count > 0 ? count : 65535;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schätzt einen angemessenen Timeout für einen masscan-Lauf über mehrere
|
||||||
|
* Hosts: Sendezeit (Gesamtzahl Probes / Rate) plus großzügiger
|
||||||
|
* Sicherheitsfaktor (x2) plus masscans eigene Nachlaufzeit für spät
|
||||||
|
* eintreffende Antworten (~15s), gedeckelt auf sinnvolle Grenzen (60s..15min)
|
||||||
|
* - lieber einmal spürbar warten als einen fast fertigen Lauf über Dutzende
|
||||||
|
* Geräte kurz vor Schluss abwürgen.
|
||||||
|
*/
|
||||||
|
function computeTimeoutMs(hostCount: number, portRange: string, rate: number): number {
|
||||||
|
const totalProbes = hostCount * countPortsInRange(portRange);
|
||||||
|
const estimatedSeconds = totalProbes / rate;
|
||||||
|
const withMargin = estimatedSeconds * 2 + 15;
|
||||||
|
return Math.min(Math.max(withMargin * 1000, 60_000), 15 * 60_000);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nutzt masscan (falls im Container installiert, siehe Dockerfile) für den
|
* Nutzt masscan (falls im Container installiert, siehe Dockerfile) für den
|
||||||
* vollständigen Portscan (1-65535) - masscan verschickt SYN-Pakete
|
* vollständigen Portscan (1-65535) EINER GANZEN GRUPPE von Hosts in einem
|
||||||
* asynchron per Rohsocket, ohne auf jede einzelne TCP-Verbindung zu warten,
|
* einzigen Prozesslauf - masscan unterstützt mehrere Ziel-IPs nativ als
|
||||||
* und ist dadurch um ein Vielfaches schneller als das eingebaute
|
* Argumente und verschickt SYN-Pakete asynchron per Rohsocket, ohne auf jede
|
||||||
* Batch-TCP-Connect-Scanning (scanPortsInBatches in ports.ts).
|
* einzelne TCP-Verbindung zu warten. Das ist der Grund, warum ein einziger
|
||||||
|
* Lauf über z. B. 20 Geräte kaum länger dauert als einer über ein einzelnes
|
||||||
|
* Gerät (die Gesamt-Paketrate ist der limitierende Faktor, nicht die Anzahl
|
||||||
|
* Hosts) - und warum das frühere Modell "ein masscan-Prozess PRO Gerät"
|
||||||
|
* unnötig langsam war.
|
||||||
*
|
*
|
||||||
* Fällt automatisch auf null zurück, wenn masscan fehlt, keine Rechte hat
|
* Fällt automatisch auf null zurück, wenn masscan fehlt, keine Rechte hat
|
||||||
* (braucht CAP_NET_RAW, siehe docker-compose.yml) oder aus anderem Grund
|
* (braucht CAP_NET_RAW, siehe docker-compose.yml) oder aus anderem Grund
|
||||||
* fehlschlägt - der Aufrufer nutzt dann stattdessen scanPortsInBatches. Kein
|
* fehlschlägt - der Aufrufer nutzt dann stattdessen scanPortsInBatches PRO
|
||||||
* hartes Erfordernis, nur eine Beschleunigung. Der genaue Grund eines
|
* GERÄT. Kein hartes Erfordernis, nur eine Beschleunigung. Der genaue Grund
|
||||||
* Fehlschlags steht danach in getMasscanLastError().
|
* eines Fehlschlags steht danach in getMasscanLastError().
|
||||||
*/
|
*/
|
||||||
export async function scanWithMasscan(
|
export async function scanHostsWithMasscan(
|
||||||
host: string,
|
hosts: string[],
|
||||||
portRange: string,
|
portRange: string,
|
||||||
rate = 2000
|
rate: number = DEFAULT_RATE
|
||||||
): Promise<number[] | null> {
|
): Promise<Map<string, number[]> | null> {
|
||||||
if (masscanConfirmedUnavailable) return null;
|
if (masscanConfirmedUnavailable || hosts.length === 0) {
|
||||||
|
return hosts.length === 0 ? new Map() : null;
|
||||||
|
}
|
||||||
|
|
||||||
await masscanSemaphore.acquire();
|
await masscanSemaphore.acquire();
|
||||||
try {
|
try {
|
||||||
|
const timeoutMs = computeTimeoutMs(hosts.length, portRange, rate);
|
||||||
return await new Promise((resolve) => {
|
return await new Promise((resolve) => {
|
||||||
const child = execFile(
|
execFile(
|
||||||
"masscan",
|
"masscan",
|
||||||
[host, "-p", portRange, "--rate", String(rate), "-oL", "-"],
|
[...hosts, "-p", portRange, "--rate", String(rate), "-oL", "-"],
|
||||||
{ timeout: MASSCAN_TIMEOUT_MS },
|
// maxBuffer hochgesetzt: bei vielen Hosts mit vielen offenen Ports
|
||||||
|
// (z. B. nach einem vollständigen 1-65535-Scan über 20+ Geräte)
|
||||||
|
// reicht das node-Standardlimit von 1MB stdout ggf. nicht.
|
||||||
|
{ timeout: timeoutMs, maxBuffer: 32 * 1024 * 1024 },
|
||||||
(error, stdout, stderr) => {
|
(error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
const wasTimeout = error.killed === true;
|
const wasTimeout = error.killed === true;
|
||||||
@@ -102,7 +146,7 @@ export async function scanWithMasscan(
|
|||||||
// Explizit als Zeitüberschreitung benennen - die mitgelieferte
|
// Explizit als Zeitüberschreitung benennen - die mitgelieferte
|
||||||
// stderr-Ausgabe ist nur der letzte Stand VOR dem Abbruch
|
// stderr-Ausgabe ist nur der letzte Stand VOR dem Abbruch
|
||||||
// (masscans eigene Fortschrittsanzeige), keine Fehlermeldung.
|
// (masscans eigene Fortschrittsanzeige), keine Fehlermeldung.
|
||||||
masscanLastError = `Zeitüberschreitung nach ${MASSCAN_TIMEOUT_MS / 1000}s - masscan wurde per ${error.signal ?? "Signal"} beendet, bevor der Scan fertig war. Letzter Stand vor Abbruch: "${lastOutput || "(keine Ausgabe)"}"`;
|
masscanLastError = `Zeitüberschreitung nach ${Math.round(timeoutMs / 1000)}s (${hosts.length} Host(s), Portbereich ${portRange}) - masscan wurde per ${error.signal ?? "Signal"} beendet, bevor der Scan fertig war. Letzter Stand vor Abbruch: "${lastOutput || "(keine Ausgabe)"}"`;
|
||||||
} else {
|
} else {
|
||||||
// Echter Fehlschlag (Exit-Code ≠ 0, nicht durch Timeout) -
|
// Echter Fehlschlag (Exit-Code ≠ 0, nicht durch Timeout) -
|
||||||
// stderr enthält bei masscan i. d. R. den eigentlichen Grund
|
// stderr enthält bei masscan i. d. R. den eigentlichen Grund
|
||||||
@@ -114,11 +158,9 @@ export async function scanWithMasscan(
|
|||||||
|
|
||||||
// ENOENT (Programm fehlt) oder fehlende Rechte (EPERM/"PCAP" o. ä.
|
// ENOENT (Programm fehlt) oder fehlende Rechte (EPERM/"PCAP" o. ä.
|
||||||
// in stderr) - beides bedeutet: masscan für diese Installation
|
// in stderr) - beides bedeutet: masscan für diese Installation
|
||||||
// dauerhaft nicht nutzbar, künftig direkt auf den Fallback gehen
|
// dauerhaft nicht nutzbar, künftig direkt auf den Fallback gehen.
|
||||||
// statt bei jedem Scan erneut den (dann meist schnell
|
// Ein Timeout zählt ausdrücklich NICHT dazu - das kann
|
||||||
// fehlschlagenden) Versuch zu machen. Ein Timeout zählt
|
// situationsabhängig sein (Netzlast) und beim nächsten Mal klappen.
|
||||||
// ausdrücklich NICHT dazu - das kann situationsabhängig sein
|
|
||||||
// (Netzlast) und beim nächsten Mal klappen.
|
|
||||||
const msg = `${error.message} ${stderr ?? ""}`.toLowerCase();
|
const msg = `${error.message} ${stderr ?? ""}`.toLowerCase();
|
||||||
if (
|
if (
|
||||||
!wasTimeout &&
|
!wasTimeout &&
|
||||||
@@ -135,26 +177,47 @@ export async function scanWithMasscan(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// "-oL -" (List-Format) gibt pro offenem Port eine Zeile aus:
|
// "-oL -" (List-Format) gibt pro offenem Port eine Zeile aus:
|
||||||
// "open tcp 80 192.168.1.50 1700000000"
|
// "open tcp 80 192.168.1.50 1700000000" - die IP-Spalte (Index 3)
|
||||||
const ports: number[] = [];
|
// ist bei mehreren Ziel-Hosts der Schlüssel, über den die Treffer
|
||||||
|
// den einzelnen Geräten zugeordnet werden.
|
||||||
|
const portsByHost = new Map<string, number[]>();
|
||||||
for (const line of stdout.split("\n")) {
|
for (const line of stdout.split("\n")) {
|
||||||
const parts = line.trim().split(/\s+/);
|
const parts = line.trim().split(/\s+/);
|
||||||
if (parts[0] === "open" && parts[1] === "tcp") {
|
if (parts[0] === "open" && parts[1] === "tcp") {
|
||||||
const port = Number(parts[2]);
|
const port = Number(parts[2]);
|
||||||
if (Number.isFinite(port)) ports.push(port);
|
const ip = parts[3];
|
||||||
|
if (Number.isFinite(port) && ip) {
|
||||||
|
const list = portsByHost.get(ip);
|
||||||
|
if (list) list.push(port);
|
||||||
|
else portsByHost.set(ip, [port]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
masscanLastError = null; // erfolgreicher Lauf - alten Fehler nicht mehr anzeigen
|
masscanLastError = null; // erfolgreicher Lauf - alten Fehler nicht mehr anzeigen
|
||||||
resolve(ports);
|
resolve(portsByHost);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
void child;
|
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
masscanSemaphore.release();
|
masscanSemaphore.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einzelgerät-Variante von scanHostsWithMasscan() - für den "Jetzt scannen"-
|
||||||
|
* Button auf der Geräteseite (ein Gerät, kein Sammel-Scan). Intern nur ein
|
||||||
|
* Aufruf von scanHostsWithMasscan() mit genau einem Host.
|
||||||
|
*/
|
||||||
|
export async function scanWithMasscan(
|
||||||
|
host: string,
|
||||||
|
portRange: string,
|
||||||
|
rate: number = DEFAULT_RATE
|
||||||
|
): Promise<number[] | null> {
|
||||||
|
const result = await scanHostsWithMasscan([host], portRange, rate);
|
||||||
|
if (result === null) return null;
|
||||||
|
return result.get(host) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
export function isMasscanConfirmedUnavailable(): boolean {
|
export function isMasscanConfirmedUnavailable(): boolean {
|
||||||
return masscanConfirmedUnavailable;
|
return masscanConfirmedUnavailable;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { resolveHostname, reverseLookup } from "./dns.js";
|
import { resolveHostname, reverseLookup } from "./dns.js";
|
||||||
import { isPortOpen, scanPortsInBatches, fullPortRange, TYPICAL_PORTS } from "./ports.js";
|
import { isPortOpen, scanPortsInBatches, fullPortRange, TYPICAL_PORTS } from "./ports.js";
|
||||||
import { scanWithMasscan, getMasscanLastError } from "./masscan.js";
|
import { scanWithMasscan, getMasscanLastError } from "./masscan.js";
|
||||||
import { probeWithHttpx, getHttpxLastError } from "./httpx.js";
|
import { probeWithHttpx, getHttpxLastError, type HttpxResult } from "./httpx.js";
|
||||||
import { probeHttp } from "./http.js";
|
import { probeHttp } from "./http.js";
|
||||||
import { detectSoftware } from "./softwareDetection.js";
|
import { detectSoftware } from "./softwareDetection.js";
|
||||||
import { findBestIconMatch } from "./iconDb.js";
|
import { findBestIconMatch } from "./iconDb.js";
|
||||||
@@ -100,6 +100,25 @@ export interface DeviceScanResult {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vorab (außerhalb dieser Funktion) ermittelte Scan-Daten für EIN Gerät -
|
||||||
|
* genutzt vom Sammel-Scan (siehe routes/scan.ts), der masscan/httpx nicht
|
||||||
|
* mehr pro Gerät einzeln aufruft, sondern EINMAL für alle Geräte des
|
||||||
|
* Scan-Laufs gemeinsam (siehe scanHostsWithMasscan/probeManyWithHttpx) und
|
||||||
|
* das Ergebnis dann pro Gerät hier einspeist. Bleibt dieser Parameter ganz
|
||||||
|
* weg (Einzelgeräte-Scan über den "Jetzt scannen"-Button), verhält sich die
|
||||||
|
* Funktion wie zuvor und macht die masscan-/httpx-Aufrufe selbst.
|
||||||
|
*/
|
||||||
|
export interface PreFetchedScanData {
|
||||||
|
masscanAttempted: boolean;
|
||||||
|
/** undefined = masscan wurde für dieses Gerät nicht vorab versucht (dann selbst versuchen). */
|
||||||
|
masscanPorts?: number[];
|
||||||
|
masscanError: string | null;
|
||||||
|
/** undefined = httpx wurde für dieses Gerät nicht vorab versucht (dann selbst versuchen). */
|
||||||
|
httpxResults?: Map<number, HttpxResult> | null;
|
||||||
|
httpxError: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scannt ein einzelnes Gerät: versucht zuerst eine schönere DNS-Adresse
|
* Scannt ein einzelnes Gerät: versucht zuerst eine schönere DNS-Adresse
|
||||||
* (hostname / hostname.home / hostname.local) aufzulösen, prüft dann Port 80,
|
* (hostname / hostname.home / hostname.local) aufzulösen, prüft dann Port 80,
|
||||||
@@ -109,7 +128,8 @@ export interface DeviceScanResult {
|
|||||||
export async function scanDeviceServices(
|
export async function scanDeviceServices(
|
||||||
device: ScanTarget,
|
device: ScanTarget,
|
||||||
extraPorts: number[] = TYPICAL_PORTS,
|
extraPorts: number[] = TYPICAL_PORTS,
|
||||||
priorityPorts: number[] = []
|
priorityPorts: number[] = [],
|
||||||
|
preFetched?: PreFetchedScanData
|
||||||
): Promise<DeviceScanResult> {
|
): Promise<DeviceScanResult> {
|
||||||
const dnsResult = await resolveHostname(device.hostname);
|
const dnsResult = await resolveHostname(device.hostname);
|
||||||
// Fällt auf die IP zurück, falls keine DNS-Variante auflösbar ist.
|
// Fällt auf die IP zurück, falls keine DNS-Variante auflösbar ist.
|
||||||
@@ -134,14 +154,28 @@ export async function scanDeviceServices(
|
|||||||
// eingebaute Scanning reicht dafür völlig.
|
// eingebaute Scanning reicht dafür völlig.
|
||||||
let usedMasscan = false;
|
let usedMasscan = false;
|
||||||
let masscanError: string | null = null;
|
let masscanError: string | null = null;
|
||||||
const masscanAttempted = candidatePorts.length > 1000;
|
let masscanAttempted: boolean;
|
||||||
let openPorts: number[];
|
let openPorts: number[];
|
||||||
if (masscanAttempted) {
|
if (preFetched) {
|
||||||
|
// Sammel-Scan: masscan wurde bereits VORAB für alle Geräte gemeinsam
|
||||||
|
// versucht (siehe routes/scan.ts) - hier nur noch das Ergebnis für DIESES
|
||||||
|
// Gerät übernehmen, kein eigener masscan-Aufruf mehr.
|
||||||
|
masscanAttempted = preFetched.masscanAttempted;
|
||||||
|
if (preFetched.masscanPorts) {
|
||||||
|
usedMasscan = true;
|
||||||
|
openPorts = preFetched.masscanPorts;
|
||||||
|
} else {
|
||||||
|
masscanError = preFetched.masscanError;
|
||||||
|
openPorts = await scanPortsInBatches(device.ip, candidatePorts);
|
||||||
|
}
|
||||||
|
} else if (candidatePorts.length > 1000) {
|
||||||
|
masscanAttempted = true;
|
||||||
const masscanResult = await scanWithMasscan(device.ip, "1-65535");
|
const masscanResult = await scanWithMasscan(device.ip, "1-65535");
|
||||||
usedMasscan = masscanResult !== null;
|
usedMasscan = masscanResult !== null;
|
||||||
if (!usedMasscan) masscanError = getMasscanLastError();
|
if (!usedMasscan) masscanError = getMasscanLastError();
|
||||||
openPorts = masscanResult ?? (await scanPortsInBatches(device.ip, candidatePorts));
|
openPorts = masscanResult ?? (await scanPortsInBatches(device.ip, candidatePorts));
|
||||||
} else {
|
} else {
|
||||||
|
masscanAttempted = false;
|
||||||
openPorts = await scanPortsInBatches(device.ip, candidatePorts);
|
openPorts = await scanPortsInBatches(device.ip, candidatePorts);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,9 +227,20 @@ export async function scanDeviceServices(
|
|||||||
// bestätigter Port wird (seit der Regression mit stillschweigend
|
// bestätigter Port wird (seit der Regression mit stillschweigend
|
||||||
// verschwundenen Diensten unter Last) trotzdem an probePort() weiter-
|
// verschwundenen Diensten unter Last) trotzdem an probePort() weiter-
|
||||||
// gereicht statt verworfen - siehe Kommentar unten.
|
// gereicht statt verworfen - siehe Kommentar unten.
|
||||||
const httpxResults = openPorts.length > 0 ? await probeWithHttpx(device.ip, openPorts) : new Map();
|
//
|
||||||
const usingHttpx = httpxResults !== null;
|
// Beim Sammel-Scan (preFetched gesetzt) wurde httpx bereits VORAB für alle
|
||||||
const httpxError = !usingHttpx ? getHttpxLastError() : null;
|
// Geräte gemeinsam aufgerufen (siehe routes/scan.ts) - hier nur noch das
|
||||||
|
// Ergebnis für DIESES Gerät übernehmen. Ports, die erst NACH dem
|
||||||
|
// vorab-Aufruf dazugekommen sind (z. B. durch den Prioritäts-Retry oben),
|
||||||
|
// fehlen dann in der vorab ermittelten Map - das ist unkritisch, sie
|
||||||
|
// durchlaufen einfach ganz normal probePort()s eigene Protokollprüfung.
|
||||||
|
const httpxResults = preFetched
|
||||||
|
? preFetched.httpxResults
|
||||||
|
: openPorts.length > 0
|
||||||
|
? await probeWithHttpx(device.ip, openPorts)
|
||||||
|
: new Map<number, HttpxResult>();
|
||||||
|
const usingHttpx = httpxResults !== null && httpxResults !== undefined;
|
||||||
|
const httpxError = preFetched ? preFetched.httpxError : !usingHttpx ? getHttpxLastError() : null;
|
||||||
// WICHTIG: openPorts wird NICHT mehr auf die von httpx bestätigten Ports
|
// WICHTIG: openPorts wird NICHT mehr auf die von httpx bestätigten Ports
|
||||||
// eingeschränkt. httpx meldet für einen Port, den es nicht innerhalb
|
// eingeschränkt. httpx meldet für einen Port, den es nicht innerhalb
|
||||||
// seines Timeouts erreicht (z. B. unter Last durch mehrere gleichzeitige
|
// seines Timeouts erreicht (z. B. unter Last durch mehrere gleichzeitige
|
||||||
@@ -209,6 +254,7 @@ export async function scanDeviceServices(
|
|||||||
// probePort() ihn ganz normal selbst (identisch zum Verhalten ganz ohne
|
// probePort() ihn ganz normal selbst (identisch zum Verhalten ganz ohne
|
||||||
// httpx), statt ihn zu überspringen.
|
// httpx), statt ihn zu überspringen.
|
||||||
|
|
||||||
|
|
||||||
const found: DiscoveredService[] = [];
|
const found: DiscoveredService[] = [];
|
||||||
|
|
||||||
// Die HTTP-Abfrage je offenem Port läuft PARALLEL, nicht mehr
|
// Die HTTP-Abfrage je offenem Port läuft PARALLEL, nicht mehr
|
||||||
|
|||||||
Reference in New Issue
Block a user