round50: masscan/httpx laufen im Sammel-Scan nur noch einmal fuer alle Geraete statt pro Geraet, Rate deutlich erhoeht

This commit is contained in:
2026-07-26 10:49:35 +02:00
parent cf31ea4402
commit 0287a7cc5c
4 changed files with 333 additions and 107 deletions

View File

@@ -4,8 +4,10 @@ import * as serviceRepo from "../db/repositories/services.js";
import * as categoryRepo from "../db/repositories/categories.js";
import * as logRepo from "../db/repositories/logs.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 { scanHostsWithMasscan, getMasscanLastError } from "../scanner/masscan.js";
import { probeManyWithHttpx, getHttpxLastError, type HttpxResult } from "../scanner/httpx.js";
import { fetchFritzBoxHosts, type FritzBoxHost } from "../scanner/fritzbox.js";
import {
startJob,
@@ -54,7 +56,7 @@ function formatToolsUsed(toolsUsed: {
* Einzelgerät-Endpunkt als auch der Sammel-Scan (siehe /api/scan/devices/all
* 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
// erkennen, welche davon diesmal nicht mehr gefunden wurden ("stale").
const servicesBeforeScan = serviceRepo.listServicesByDevice(device.id);
@@ -62,7 +64,8 @@ async function performDeviceScan(device: Device, fullScan = false) {
const { services: discovered, suggestedHostname, toolsUsed } = await scanDeviceServices(
device,
fullScan ? fullPortRange() : undefined,
servicesBeforeScan.map((s) => s.port)
servicesBeforeScan.map((s) => s.port),
preFetched
);
// 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;
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 -
// bei z. B. 118 Geräten und je 10-30 Sekunden pro vollständigem
// Portscan hätte ein rein sequenzieller Durchlauf zusammen leicht
// 30+ Minuten gedauert. Batch-Größe bewusst moderat (nicht alle
// bei z. B. 118 Geräten hätte ein rein sequenzieller Durchlauf der
// (jetzt viel schnelleren, weil rein HTTP-basierten) Detailabfragen
// trotzdem lange gedauert. Batch-Größe bewusst moderat (nicht alle
// Geräte auf einmal): jeder einzelne Geräte-Scan öffnet intern schon
// bis zu 1000 gleichzeitige Verbindungen (siehe ports.ts) - zu viele
// Geräte gleichzeitig könnte das eigene Netz/den Docker-Host
@@ -258,7 +299,28 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
return;
}
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;
updated += result.updated;
allStale.push(...result.staleServices);