diff --git a/apps/backend/Dockerfile b/apps/backend/Dockerfile index d76ef71..6e901da 100644 --- a/apps/backend/Dockerfile +++ b/apps/backend/Dockerfile @@ -35,7 +35,11 @@ ENV HOST=0.0.0.0 # iputils-ping stellt einen "echten" ping-Befehl mit ICMP-Unterstützung # bereit (nicht nur die BusyBox-Variante) - für den optionalen Live-Status- # Heartbeat (siehe src/liveStatus.ts, standardmäßig deaktiviert). -RUN apk add --no-cache iputils-ping +# masscan beschleunigt den vollständigen Portscan (1-65535, siehe +# scanner/masscan.ts) erheblich gegenüber reinem TCP-Connect-Scanning - wird +# automatisch genutzt, falls vorhanden, sonst fällt der Scanner auf die +# eingebaute (langsamere, aber ohne Zusatzrechte auskommende) Methode zurück. +RUN apk add --no-cache iputils-ping masscan COPY --from=build /app/deploy/ ./ COPY --from=build /app/apps/backend/dist ./dist diff --git a/apps/backend/src/scanner/masscan.ts b/apps/backend/src/scanner/masscan.ts new file mode 100644 index 0000000..3bf3e64 --- /dev/null +++ b/apps/backend/src/scanner/masscan.ts @@ -0,0 +1,68 @@ +import { execFile } from "node:child_process"; + +let masscanConfirmedUnavailable = false; + +/** + * Nutzt masscan (falls im Container installiert, siehe Dockerfile) für den + * vollständigen Portscan (1-65535) - masscan verschickt SYN-Pakete + * asynchron per Rohsocket, ohne auf jede einzelne TCP-Verbindung zu warten, + * und ist dadurch um ein Vielfaches schneller als das eingebaute + * Batch-TCP-Connect-Scanning (scanPortsInBatches in ports.ts). + * + * Fällt automatisch (und leise) auf null zurück, wenn masscan fehlt, keine + * Rechte hat (braucht CAP_NET_RAW, siehe docker-compose.yml) oder aus + * anderem Grund fehlschlägt - der Aufrufer nutzt dann stattdessen + * scanPortsInBatches. Kein hartes Erfordernis, nur eine Beschleunigung. + */ +export async function scanWithMasscan( + host: string, + portRange: string, + rate = 2000 +): Promise { + if (masscanConfirmedUnavailable) return null; + + return new Promise((resolve) => { + execFile( + "masscan", + [host, "-p", portRange, "--rate", String(rate), "-oL", "-"], + { timeout: 60_000 }, + (error, stdout, stderr) => { + if (error) { + // ENOENT (Programm fehlt) oder fehlende Rechte (EPERM/"PCAP" o. ä. + // in stderr) - beides bedeutet: masscan für diese Installation + // dauerhaft nicht nutzbar, künftig direkt auf den Fallback gehen + // statt bei jedem Scan erneut den (dann meist schnell fehlschlagenden) + // Versuch zu machen. + const msg = `${error.message} ${stderr ?? ""}`.toLowerCase(); + if ( + msg.includes("enoent") || + msg.includes("not found") || + msg.includes("permission") || + msg.includes("operation not permitted") || + msg.includes("pcap") + ) { + masscanConfirmedUnavailable = true; + } + resolve(null); + return; + } + + // "-oL -" (List-Format) gibt pro offenem Port eine Zeile aus: + // "open tcp 80 192.168.1.50 1700000000" + const ports: number[] = []; + for (const line of stdout.split("\n")) { + const parts = line.trim().split(/\s+/); + if (parts[0] === "open" && parts[1] === "tcp") { + const port = Number(parts[2]); + if (Number.isFinite(port)) ports.push(port); + } + } + resolve(ports); + } + ); + }); +} + +export function isMasscanConfirmedUnavailable(): boolean { + return masscanConfirmedUnavailable; +} diff --git a/apps/backend/src/scanner/networkScanner.ts b/apps/backend/src/scanner/networkScanner.ts index 51f842a..c7aea9e 100644 --- a/apps/backend/src/scanner/networkScanner.ts +++ b/apps/backend/src/scanner/networkScanner.ts @@ -1,5 +1,6 @@ import { resolveHostname, reverseLookup } from "./dns.js"; import { isPortOpen, scanPortsInBatches, fullPortRange, TYPICAL_PORTS } from "./ports.js"; +import { scanWithMasscan } from "./masscan.js"; import { probeHttp } from "./http.js"; import { detectSoftware } from "./softwareDetection.js"; import { findBestIconMatch } from "./iconDb.js"; @@ -106,7 +107,18 @@ export async function scanDeviceServices( // (~30 Ports) macht das kaum einen Unterschied, ist aber zwingend nötig für // den optionalen VOLLSTÄNDIGEN Portscan (1-65535, siehe Admin -> Geräte), // wo alle Ports gleichzeitig öffnen würde. - let openPorts = await scanPortsInBatches(device.ip, candidatePorts); + // Bei einer großen Portliste (voller Scan, 1-65535) lohnt sich masscan, + // falls installiert (siehe scanner/masscan.ts + Dockerfile) - deutlich + // schneller als das eingebaute Batch-Scanning. Bei der kurzen "üblichen + // Ports"-Liste (~30 Ports) macht masscan kaum einen Unterschied, das + // eingebaute Scanning reicht dafür völlig. + let openPorts: number[]; + if (candidatePorts.length > 1000) { + const masscanResult = await scanWithMasscan(device.ip, "1-65535"); + openPorts = masscanResult ?? (await scanPortsInBatches(device.ip, candidatePorts)); + } else { + openPorts = await scanPortsInBatches(device.ip, candidatePorts); + } // Bereits bekannte Dienst-Ports (priorityPorts, vom Aufrufer übergeben - // siehe scan.ts), die im ersten, stark parallelen Durchlauf NICHT als diff --git a/docker-compose.yml b/docker-compose.yml index 5c5b01c..cf2c834 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,14 @@ services: - ./apps/backend/plugins:/app/plugins:ro ports: - "3001:3001" + # Für den optionalen, per masscan beschleunigten vollständigen Portscan + # (siehe scanner/masscan.ts) - masscan braucht Zugriff auf rohe Sockets. + # Ohne diese Capabilities funktioniert LaunchPad trotzdem ganz normal, + # der Portscan fällt dann automatisch auf die eingebaute (langsamere) + # Methode zurück - kein Muss, aber empfohlen für Tempo. + cap_add: + - NET_RAW + - NET_ADMIN frontend: build: