round44: Optionale masscan-Beschleunigung fuer den vollstaendigen Portscan mit automatischem Fallback

This commit is contained in:
2026-07-25 13:05:41 +02:00
parent d4ff497c8c
commit 59bb4a30b8
4 changed files with 94 additions and 2 deletions

View File

@@ -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<number[] | null> {
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;
}

View File

@@ -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