generated from Dicken/dickendock
round35: Vollstaendiger Portscan (1-65535) pro Geraet als Option, fuer Geraete mit vielen Diensten auf unueblichen Ports
This commit is contained in:
@@ -5,6 +5,7 @@ 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 { fullPortRange } from "../scanner/ports.js";
|
||||
import { fetchFritzBoxHosts, type FritzBoxHost } from "../scanner/fritzbox.js";
|
||||
import {
|
||||
startJob,
|
||||
@@ -26,14 +27,14 @@ import type { Device } from "@launchpad/shared";
|
||||
* 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) {
|
||||
async function performDeviceScan(device: Device, fullScan = false) {
|
||||
// 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);
|
||||
|
||||
const { services: discovered, suggestedHostname } = await scanDeviceServices(
|
||||
device,
|
||||
undefined,
|
||||
fullScan ? fullPortRange() : undefined,
|
||||
servicesBeforeScan.map((s) => s.port)
|
||||
);
|
||||
|
||||
@@ -138,6 +139,7 @@ async function performDeviceScan(device: Device) {
|
||||
export async function scanRoutes(app: FastifyInstance): Promise<void> {
|
||||
app.post("/api/scan/devices/:id", async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const { full } = request.query as { full?: string };
|
||||
const device = deviceRepo.getDevice(id);
|
||||
if (!device) {
|
||||
return reply.code(404).send({ error: "Gerät nicht gefunden" });
|
||||
@@ -148,7 +150,7 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
|
||||
}
|
||||
|
||||
try {
|
||||
return await performDeviceScan(device);
|
||||
return await performDeviceScan(device, full === "true");
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
logRepo.logScan({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { resolveHostname, reverseLookup } from "./dns.js";
|
||||
import { isPortOpen, TYPICAL_PORTS } from "./ports.js";
|
||||
import { isPortOpen, scanPortsInBatches, fullPortRange, TYPICAL_PORTS } from "./ports.js";
|
||||
import { probeHttp } from "./http.js";
|
||||
import { detectSoftware } from "./softwareDetection.js";
|
||||
import { findBestIconMatch } from "./iconDb.js";
|
||||
@@ -102,17 +102,11 @@ export async function scanDeviceServices(
|
||||
|
||||
const candidatePorts = Array.from(new Set([80, 443, ...extraPorts, ...priorityPorts]));
|
||||
|
||||
// Die offenen Ports werden PARALLEL geprüft, nicht nacheinander - bei
|
||||
// einem nicht erreichbaren/gefilterten Gerät (z. B. ein schlafender
|
||||
// Laptop) würde ein sequenzieller Durchlauf sonst bis zu
|
||||
// Portanzahl × Timeout dauern (bei 20 Ports und 800ms Timeout: 16+
|
||||
// Sekunden PRO GERÄT) und den Scan wie hängengeblieben wirken lassen.
|
||||
// Danach wird nur noch für die tatsächlich offenen Ports (meist wenige)
|
||||
// die eigentliche HTTP-Abfrage gemacht.
|
||||
const openChecks = await Promise.all(
|
||||
candidatePorts.map(async (port) => ({ port, open: await isPortOpen(device.ip, port) }))
|
||||
);
|
||||
let openPorts = openChecks.filter((c) => c.open).map((c) => c.port);
|
||||
// Batches statt alles auf einmal parallel - bei der "typischen Ports"-Liste
|
||||
// (~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);
|
||||
|
||||
// Bereits bekannte Dienst-Ports (priorityPorts, vom Aufrufer übergeben -
|
||||
// siehe scan.ts), die im ersten, stark parallelen Durchlauf NICHT als
|
||||
|
||||
@@ -36,3 +36,36 @@ export function isPortOpen(host: string, port: number, timeoutMs = 800): Promise
|
||||
socket.once("error", () => finish(false));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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. Ein Batch von 500
|
||||
* ist ein guter Mittelweg für ein Gerät im lokalen Netz.
|
||||
*/
|
||||
export async function scanPortsInBatches(
|
||||
host: string,
|
||||
ports: number[],
|
||||
batchSize = 500,
|
||||
timeoutMs = 600
|
||||
): 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) }))
|
||||
);
|
||||
for (const r of results) if (r.open) open.push(r.port);
|
||||
}
|
||||
return open;
|
||||
}
|
||||
|
||||
/** Alle Ports von 1 bis 65535 - für den vollständigen (statt nur "typische Ports") Scan eines einzelnen Geräts. */
|
||||
export function fullPortRange(): number[] {
|
||||
const ports: number[] = [];
|
||||
for (let p = 1; p <= 65535; p++) ports.push(p);
|
||||
return ports;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState, type FormEvent } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faBan, faCheck, faFloppyDisk, faPen, faTrash, faXmark, faSort, faSortUp, faSortDown } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faBan, faCheck, faFloppyDisk, faPen, faTrash, faXmark, faSort, faSortUp, faSortDown, faMagnifyingGlassPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Button, Favicon } from "@launchpad/ui";
|
||||
import type { Service } from "@launchpad/shared";
|
||||
@@ -79,8 +79,8 @@ interface ScanResult {
|
||||
deviceNameSuggestion: string | null;
|
||||
}
|
||||
|
||||
async function scanDevice(id: string, signal?: AbortSignal): Promise<ScanResult> {
|
||||
const res = await fetch(`/api/scan/devices/${id}`, { method: "POST", signal });
|
||||
async function scanDevice(id: string, signal?: AbortSignal, full = false): Promise<ScanResult> {
|
||||
const res = await fetch(`/api/scan/devices/${id}${full ? "?full=true" : ""}`, { method: "POST", signal });
|
||||
const body = await res.json();
|
||||
if (!res.ok) {
|
||||
throw new Error(body.detail ?? body.error ?? `Scan fehlgeschlagen (HTTP ${res.status})`);
|
||||
@@ -326,10 +326,10 @@ function DeviceRow({ device }: { device: DeviceWithServices }) {
|
||||
const [scanAbort, setScanAbort] = useState<AbortController | null>(null);
|
||||
|
||||
const scanMutation = useMutation({
|
||||
mutationFn: () => {
|
||||
mutationFn: (full: boolean) => {
|
||||
const controller = new AbortController();
|
||||
setScanAbort(controller);
|
||||
return scanDevice(device.id, controller.signal);
|
||||
return scanDevice(device.id, controller.signal, full);
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
const portsText = result.ports.length > 0 ? result.ports.join(", ") : "keine";
|
||||
@@ -414,12 +414,25 @@ function DeviceRow({ device }: { device: DeviceWithServices }) {
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setScanMessage(null);
|
||||
scanMutation.mutate();
|
||||
scanMutation.mutate(false);
|
||||
}}
|
||||
disabled={scanMutation.isPending}
|
||||
>
|
||||
{scanMutation.isPending ? "Scanne …" : "Jetzt scannen"}
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setScanMessage("Vollständiger Portscan (1-65535) läuft, kann etwas dauern …");
|
||||
scanMutation.mutate(true);
|
||||
}}
|
||||
disabled={scanMutation.isPending}
|
||||
title="Vollständigen Portscan (alle 65535 Ports statt nur der üblichen) durchführen - für Geräte mit vielen Diensten auf unüblichen Ports"
|
||||
aria-label="Vollständigen Portscan durchführen"
|
||||
>
|
||||
<FontAwesomeIcon icon={faMagnifyingGlassPlus} />
|
||||
</Button>
|
||||
{scanMutation.isPending && scanAbort ? (
|
||||
<Button
|
||||
size="icon"
|
||||
|
||||
Reference in New Issue
Block a user