Files
LaunchPad/apps/backend/src/scanner/dns.ts

81 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<T>(promise: Promise<T>, ms: number): Promise<T> {
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<DnsResolution | null> {
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<string | null> {
try {
const names = await withTimeout(reverse(ip), DNS_TIMEOUT_MS);
return names[0] ?? null;
} catch {
return null;
}
}