import { lookup, reverse } from "node:dns/promises"; export interface DnsResolution { hostname: string; ip: string; } // GEFUNDENER FLASCHENHALS (round55): dns.lookup()/dns.reverse() aus // node:dns/promises laufen NICHT direkt asynchron über den Netzwerk-Stack, // sondern über getaddrinfo() im Betriebssystem - Node schickt das über den // libuv-Threadpool ab, der standardmäßig nur 4 Threads hat // (UV_THREADPOOL_SIZE=4), UNABHÄNGIG von Node's eigentlich nicht-blockierendem // I/O-Modell. Ohne eigenen Timeout kann eine einzelne langsame/hängende // Abfrage (z. B. für Handys/Smart-Geräte mit FritzBox-Spitznamen wie // "S24-Ultra-von-Sebastian", die weder per normaler DNS noch per .home/.local // auflösbar sind, aber je nach Resolver/Musl-libc im Alpine-Image erst nach // langer Zeit "nicht gefunden" zurückmelden) einen der 4 Threads über // Sekunden bis Minuten blockieren. Bei bis zu 10 Geräten gleichzeitig (siehe // routes/scan.ts CONCURRENT_DEVICES) mit je bis zu 4 DNS-Abfragen (3x // resolveHostname-Varianten + 1x reverseLookup) stauen sich die restlichen // Abfragen dann VOR dem 4er-Nadelöhr - das erklärt einen Sammel-Scan, der // nach einem erfolgreichen masscan-/httpx-Vorabscan plötzlich minutenlang in // der eigentlichen Geräte-Schleife hängt, obwohl dort eigentlich nur noch // leichte HTTP-Detailabfragen laufen sollten (siehe Bugreport). const DNS_TIMEOUT_MS = 1500; function withTimeout(promise: Promise, ms: number): Promise { return new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error("DNS-Zeitüberschreitung")), ms); promise.then( (value) => { clearTimeout(timer); resolve(value); }, (err) => { clearTimeout(timer); reject(err); } ); }); } /** * Versucht der Spezifikation folgend: hostname, hostname.home, hostname.local. * Gibt die erste erfolgreich aufgelöste Variante zurück, sonst null. Jeder * einzelne Versuch ist auf DNS_TIMEOUT_MS begrenzt (siehe Kommentar oben) - * eine hängende Abfrage blockiert damit höchstens 3 × 1,5s statt potenziell * unbegrenzt. */ export async function resolveHostname(shortName: string): Promise { const candidates = [shortName, `${shortName}.home`, `${shortName}.local`]; for (const hostname of candidates) { try { const { address } = await withTimeout(lookup(hostname), DNS_TIMEOUT_MS); return { hostname, ip: address }; } catch { // nächste Variante versuchen (Fehler ODER Timeout) } } 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. Mit * demselben Timeout abgesichert wie resolveHostname() oben. */ export async function reverseLookup(ip: string): Promise { try { const names = await withTimeout(reverse(ip), DNS_TIMEOUT_MS); return names[0] ?? null; } catch { return null; } }