Du willst wissen, wo eine fremde Domain liegt. WHOIS hilft dabei kaum noch: seit der DSGVO stehen bei den meisten TLDs nur der Registrar und geschwärzte Felder. Der Registrar ist aber nicht der Hoster. Eine Domain kann bei einem Registrar verwaltet werden und auf einem Server ganz woanders laufen.
Die brauchbare Antwort steht in den NS-Records. Die bekommst du im Browser, ohne Server und ohne API-Key.
Getestet mit
- Google Public DNS JSON API (
dns.google/resolve), Stand 08/2026 - Cloudflare DNS over HTTPS JSON (
cloudflare-dns.com/dns-query), Stand 08/2026 - Firefox 153, Chrome 151
- reines JavaScript im Browser, kein Build, kein Backend
Schritt 1: DNS-Abfrage per DNS-over-HTTPS
Google Public DNS beantwortet GET-Anfragen mit JSON. Es braucht keinen zusätzlichen Header, damit bleibt die Anfrage ein einfacher Cross-Origin-Request.
const DOH_ENDPOINT = "https://dns.google/resolve";
async function doh(name, type) {
const url = DOH_ENDPOINT
+ "?name=" + encodeURIComponent(name)
+ "&type=" + encodeURIComponent(type);
const res = await fetch(url);
if (!res.ok) {
throw new Error("DoH-Anfrage fehlgeschlagen: HTTP " + res.status);
}
const json = await res.json();
if (json.Status === 3) {
throw new Error("Domain existiert nicht (NXDOMAIN): " + name);
}
if (json.Status !== 0) {
throw new Error("DNS-Fehler " + json.Status + " bei " + name + " " + type);
}
return json.Answer || [];
}
Status ist der DNS-Response-Code: 0 ist NOERROR, 3 ist NXDOMAIN. Eine Domain ohne AAAA- oder MX-Eintrag liefert Status 0 und ein leeres Answer, das ist kein Fehler.
Cloudflare liefert dasselbe Schema unter https://cloudflare-dns.com/dns-query, verlangt aber den Header accept: application/dns-json. Für das JSON-Format gibt es kein RFC, Cloudflare orientiert sich laut eigener Doku am Schema von Google. Kleine Unterschiede zwischen Anbietern sind also möglich.
Schritt 2: Die richtigen Record-Typen herausfiltern
Das Answer-Array enthält nicht nur den Typ, den du abgefragt hast. Steht auf der Domain ein CNAME, liegen dort auch CNAME-Einträge (Typ 5). Wer blind über Answer läuft, hat die im Ergebnis stehen.
const RR = { A: 1, NS: 2, CNAME: 5, MX: 15, AAAA: 28 };
function stripDot(value) {
return value.replace(/.$/, "");
}
function records(answer, type) {
return answer
.filter((entry) => entry.type === type)
.map((entry) => stripDot(entry.data.trim()));
}
function mailHosts(answer) {
return records(answer, RR.MX).map((data) => data.split(/s+/)[1]);
}
function normalizeDomain(input) {
return input
.trim()
.toLowerCase()
.replace(/^https?:///, "")
.replace(//.*$/, "")
.replace(/^www./, "");
}
Zwei Details, die sonst Ärger machen: DNS-Namen kommen mit abschließendem Punkt zurück (ns1.example.com.), und ein MX-Record steckt als ein String samt Priorität im Feld data (10 mx.example.com.). Deshalb erst den Punkt abschneiden, dann am Leerzeichen trennen.
Das www. muss weg, bevor du fragst. NS-Records liegen auf der Zone, nicht auf einer Subdomain.
Schritt 3: Nameserver einem Anbieter zuordnen
Dafür gibt es keine API. Die Zuordnung ist eine Liste, die du selbst pflegst. Das ist der ehrliche Preis dieser Lösung: die Abfrage kostet nichts, die Liste kostet Wartung.
const HOSTERS = [
{ pattern: /your-server.de|first-ns.de|second-ns.(com|de)/i, name: "Hetzner" },
{ pattern: /ionos|ui-dns|1and1|oneandone/i, name: "IONOS (1&1)" },
{ pattern: /strato/i, name: "STRATO" },
{ pattern: /netcup/i, name: "netcup" },
{ pattern: /world4you/i, name: "World4You" },
{ pattern: /easyname/i, name: "easyname" },
{ pattern: /kasserver/i, name: "ALL-INKL" },
{ pattern: /hosteurope/i, name: "Host Europe" },
{ pattern: /mittwald/i, name: "Mittwald" },
{ pattern: /df.eu|domainfactory/i, name: "domainfactory" },
{ pattern: /hosttech/i, name: "hosttech" },
{ pattern: /awsdns/i, name: "Amazon Route 53" },
{ pattern: /azure-dns/i, name: "Microsoft Azure" },
{ pattern: /googledomains|nsd?.google.com/i, name: "Google" },
{ pattern: /nsone.net/i, name: "NS1 / IBM" },
{ pattern: /domaincontrol.com/i, name: "GoDaddy" },
{ pattern: /registrar-servers.com/i, name: "Namecheap" },
{ pattern: /digitalocean/i, name: "DigitalOcean" },
{ pattern: /vultr/i, name: "Vultr" },
{ pattern: /linode|akamai/i, name: "Akamai / Linode" },
{ pattern: /wixdns/i, name: "Wix" },
{ pattern: /squarespacedns/i, name: "Squarespace" },
{ pattern: /cloudflare/i, name: "Cloudflare" }
];
function detect(list, haystack) {
const hit = list.find((item) => item.pattern.test(haystack));
return hit ? hit.name : null;
}
Die Liste wird von oben nach unten geprüft, der erste Treffer gewinnt. Spezifische Muster gehören deshalb nach oben und weit gefasste nach unten. Ein Muster wie /one.com/ ohne Anker trifft auch mitten in einem längeren Namen.
Schritt 4: Mailanbieter über die MX-Records
Dieselbe Mechanik, andere Liste. Der Mailanbieter ist oft ein anderer als der Hoster, und genau das ist die interessante Information.
const MAILHOSTS = [
{ pattern: /protection.outlook.com|outlook.com/i, name: "Microsoft 365" },
{ pattern: /aspmx.*google.com|googlemail.com/i, name: "Google Workspace" },
{ pattern: /mailgun/i, name: "Mailgun" },
{ pattern: /sendgrid/i, name: "SendGrid" },
{ pattern: /zoho/i, name: "Zoho Mail" },
{ pattern: /protonmail|proton.me/i, name: "Proton Mail" },
{ pattern: /mailbox.org/i, name: "mailbox.org" },
{ pattern: /messagingengine/i, name: "Fastmail" },
{ pattern: /your-server.de|hetzner/i, name: "Hetzner Mail" },
{ pattern: /kasserver/i, name: "ALL-INKL Mail" },
{ pattern: /brevo|sendinblue/i, name: "Brevo" }
];
async function lookup(rawDomain) {
const domain = normalizeDomain(rawDomain);
const [nsAnswer, aAnswer, aaaaAnswer, mxAnswer] = await Promise.all([
doh(domain, "NS"),
doh(domain, "A"),
doh(domain, "AAAA"),
doh(domain, "MX")
]);
const nameservers = records(nsAnswer, RR.NS);
const mx = mailHosts(mxAnswer);
return {
domain: domain,
nameservers: nameservers,
ipv4: records(aAnswer, RR.A),
ipv6: records(aaaaAnswer, RR.AAAA),
mx: mx,
dnsAnbieter: detect(HOSTERS, nameservers.join(" ")),
mailAnbieter: detect(MAILHOSTS, mx.join(" "))
};
}
Die vier Abfragen laufen parallel. Existiert die Domain nicht, wirft die erste Abfrage NXDOMAIN und Promise.all bricht alles ab. Das ist so gewollt, weil du dann ohnehin nichts anzuzeigen hast.
Der Umweg über RDAP
RDAP ist der Nachfolger von WHOIS: JSON über HTTPS, sauber spezifiziert. Das klingt nach genau dem, was ein Browser-Tool braucht, und ich habe zuerst darauf gesetzt. Zwei Dinge standen im Weg.
Das erste ist der Bootstrap. Du musst wissen, welcher RDAP-Server für eine TLD zuständig ist. rdap.org nimmt einem das ab und leitet weiter, steht aber hinter Cloudflare und begrenzt auf zehn Anfragen in zehn Sekunden. Darüber kommt 429, bei Missbrauchsverdacht 403. Für ein Tool, das pro Abfrage mehrere Requests absetzt, ist das knapp.
Das zweite hat mich länger aufgehalten. Viele Registry-Endpunkte schicken kein Access-Control-Allow-Origin. Der Server antwortet mit 200, der Browser wirft die Antwort trotzdem weg.
Gesehen habe ich es über den Netzwerk-Tab. Dort stand Status 200, während fetch() mit TypeError: Failed to fetch abbrach. Erst die Konsole nennt den fehlenden Header. Wer nur auf den Statuscode schaut, sucht den Fehler im eigenen Code.
Seitdem prüfe ich bei jedem fremden Endpunkt zuerst, ob er überhaupt für den Browser gedacht ist:
curl -sIL "https://rdap.org/domain/DEINE-DOMAIN.AT" | grep -i access-control
Kommt nichts zurück, brauchst du einen eigenen Fetch-Endpoint auf dem Server. Oder einen anderen Weg. Ich habe den anderen Weg genommen.
Kontrolle
Zuerst der Endpunkt selbst, ohne JavaScript:
curl "https://dns.google/resolve?name=DEINE-DOMAIN.AT&type=NS"
Erwartete Ausgabe, gekürzt auf die Felder, auf die es ankommt:
{
"Status": 0,
"Question": [ { "name": "DEINE-DOMAIN.AT.", "type": 2 } ],
"Answer": [
{ "name": "DEINE-DOMAIN.AT.", "type": 2, "TTL": 3600, "data": "ns1.first-ns.de." },
{ "name": "DEINE-DOMAIN.AT.", "type": 2, "TTL": 3600, "data": "robotns2.second-ns.de." }
]
}
"Status": 0 und mindestens ein Eintrag mit "type": 2 heißt, die Abfrage steht. Dann in der Browser-Konsole:
await lookup("DEINE-DOMAIN.AT");
Im Ergebnisobjekt müssen nameservers gefüllt und dnsAnbieter gesetzt sein. Steht dort null, fehlt das Muster in deiner Liste, und die Nameserver-Namen sagen dir, welches du ergänzen musst.
Häufige Fehler
Ich bekomme gar keine Nameserver zurück. Du fragst wahrscheinlich eine Subdomain ab. NS-Records liegen auf der Zone, www.example.at hat keine eigenen. normalizeDomain() schneidet nur www. ab, alle anderen Subdomains musst du selbst entfernen.
Der angezeigte Hoster stimmt nicht. Die NS-Records nennen den DNS-Anbieter, nicht zwingend den Webhoster. Wer Cloudflare davorschaltet, taucht als Cloudflare auf, egal wo die Seite tatsächlich läuft. Für den Webserver musst du über die IP aus dem A-Record gehen, und die gehört dann oft einem Rechenzentrum statt einem Hoster.
Kann ich statt Google auch Cloudflare abfragen? Ja, das Schema ist dasselbe. Du brauchst den Header accept: application/dns-json. Weil es für das JSON-Format kein RFC gibt, solltest du dich nicht auf identisches Verhalten beider Anbieter verlassen.
Warum nicht einfach WHOIS abfragen? WHOIS läuft über Port 43 im Klartext, dafür hat der Browser keinen Client. RDAP wäre der HTTP-Weg, scheitert aber an den fehlenden CORS-Headern.
Ist das datenschutzfreundlich? Die abgefragte Domain geht an Google beziehungsweise Cloudflare, die IP des Besuchers ebenfalls. Wenn dich das stört, setz einen eigenen DoH-Resolver davor. Das Schema bleibt gleich, es ändert sich nur die URL in DOH_ENDPOINT.