Fix: FritzBox faelschlich als Home Assistant erkannt + Reverse-DNS + Namens-Aenderungsvorschlaege beim Scan

This commit is contained in:
2026-07-22 15:58:10 +02:00
parent 7cf64404c5
commit 5f150119b2
8 changed files with 361 additions and 15 deletions

View File

@@ -1,4 +1,4 @@
import { lookup } from "node:dns/promises";
import { lookup, reverse } from "node:dns/promises";
export interface DnsResolution {
hostname: string;
@@ -23,3 +23,19 @@ export async function resolveHostname(shortName: string): Promise<DnsResolution
return null;
}
/**
* Umgekehrte DNS-Abfrage (PTR-Record): fragt anhand der IP nach einem Namen.
* Viele Router/DHCP-Server (FritzBox, Pi-hole, AdGuard Home, ...) tragen hier
* automatisch den vom Gerät gemeldeten DHCP-Hostnamen ein, auch wenn die
* normale Vorwärtsauflösung (Name -> IP) nichts findet. Nützlich als
* zusätzliche Quelle für Gerätenamen, siehe scanner/networkScanner.ts.
*/
export async function reverseLookup(ip: string): Promise<string | null> {
try {
const names = await reverse(ip);
return names[0] ?? null;
} catch {
return null;
}
}

View File

@@ -1,4 +1,4 @@
import { resolveHostname } from "./dns.js";
import { resolveHostname, reverseLookup } from "./dns.js";
import { isPortOpen, TYPICAL_PORTS } from "./ports.js";
import { probeHttp } from "./http.js";
import { detectSoftware } from "./softwareDetection.js";
@@ -37,6 +37,17 @@ function isLikelyJunkTitle(title: string | undefined): boolean {
return false;
}
export interface DeviceScanResult {
services: DiscoveredService[];
/**
* Vorschlag für einen besseren Gerätenamen per Reverse-DNS (PTR), falls die
* normale Vorwärtsauflösung nichts Besseres als die reine IP gefunden hat.
* Wird NICHT automatisch übernommen - nur zur manuellen Bestätigung
* zurückgegeben (siehe routes/scan.ts).
*/
suggestedHostname: string | null;
}
/**
* Scannt ein einzelnes Gerät: versucht zuerst eine schönere DNS-Adresse
* (hostname / hostname.home / hostname.local) aufzulösen, prüft dann Port 80,
@@ -46,11 +57,17 @@ function isLikelyJunkTitle(title: string | undefined): boolean {
export async function scanDeviceServices(
device: ScanTarget,
extraPorts: number[] = TYPICAL_PORTS
): Promise<DiscoveredService[]> {
): Promise<DeviceScanResult> {
const dnsResult = await resolveHostname(device.hostname);
// Fällt auf die IP zurück, falls keine DNS-Variante auflösbar ist.
const address = dnsResult?.hostname ?? device.ip;
// Nur wenn die Vorwärtsauflösung nichts Besseres als die IP gefunden hat,
// zusätzlich per Reverse-DNS (PTR) versuchen - viele Router/DHCP-Server
// (FritzBox, Pi-hole, AdGuard Home, ...) tragen hier den vom Gerät
// gemeldeten Namen ein.
const suggestedHostname = dnsResult ? null : await reverseLookup(device.ip);
const candidatePorts = Array.from(new Set([80, 443, ...extraPorts]));
const found: DiscoveredService[] = [];
@@ -65,6 +82,7 @@ export async function scanDeviceServices(
const software = detectSoftware({
server: probe.server,
body: probe.bodySnippet,
title: probe.title,
port,
});
@@ -90,5 +108,5 @@ export async function scanDeviceServices(
});
}
return found;
return { services: found, suggestedHostname };
}

View File

@@ -1,6 +1,7 @@
export interface SoftwareSignatureInput {
server?: string;
body?: string;
title?: string;
port: number;
}
@@ -16,12 +17,29 @@ function bodyContains(input: SoftwareSignatureInput, pattern: RegExp): boolean {
return !!input.body && pattern.test(input.body);
}
function titleMatches(input: SoftwareSignatureInput, pattern: RegExp): boolean {
return !!input.title && pattern.test(input.title.trim());
}
/**
* Automatische Softwareerkennung anhand von HTTP-Response-Merkmalen
* (Server-Header, HTML-Inhalt). Liste gemäß Spezifikation.
*
* Grundsatz: der `<title>`-Tag ist ein viel präziseres Signal als der
* komplette Seiteninhalt und wird deshalb wo möglich zuerst geprüft. Reine
* Volltextsuche im Body (bodyContains) besonders bei Marken-/Produktnamen
* aus generischen Alltagswörtern (z. B. "Home Assistant") kann in
* minifiziertem JavaScript anderer Web-UIs zufällig zutreffen - siehe
* Bugreport: FritzBox-Repeater wurden fälschlich als "Home Assistant"
* erkannt, weil beide Wörter irgendwo im ausgelieferten JS vorkamen.
*/
export const SOFTWARE_SIGNATURES: SoftwareSignature[] = [
{ name: "Home Assistant", category: "Smart Home", matches: (i) => bodyContains(i, /home\s*assistant/i) },
{
name: "Home Assistant",
category: "Smart Home",
matches: (i) =>
titleMatches(i, /^home\s*assistant$/i) || bodyContains(i, /frontend_latest\//i),
},
{
name: "Synology DSM",
category: "NAS",