🪜 How to check a delegation
A delegation is the handoff where one zone says "everything under this name is somebody else's job now." Two different servers hold an opinion about who that somebody is. When those opinions differ, resolution becomes a coin flip.
Background on the concept is in delegations. This page is the check. Every step runs on four levels: the dig you type, the Wireshark filter that isolates exactly that traffic, the tcpdump line that catches it on a server with no GUI, and the dnstap frame or log line the servers themselves write. Working filters for everything DNS-shaped live in wireshark-dns-filters.
On this page:
- Step 1: ask the parent: get the referral, the parent's opinion of who runs the zone
- Step 2: ask the child: the zone's own opinion, with the
aaflag to prove it - Step 3: compare: the two lists side by side, and what each kind of mismatch does
- Step 4: test every server: a name on the list means nothing until the box behind it answers
- Glue: the chicken-and-egg case, and the additional section that solves it
- Quick reference: the paste-one-block version, plus every filter on this page in one table
The idea in one picture#
.net (the parent) "isitdns.net? Ask these two servers."
|
v
isitdns.net (the child) "I am isitdns.net, and my nameservers are these two."Two separate places store a list of nameservers for the same zone. The parent stores it as part of the handoff. The child stores it inside its own zone as NS records. This split is built into the protocol: RFC 1034 §4.2.1 marks the boundary of a zone, the "cut", with NS records held by the parent, while the child keeps its own copy at the apex. Nobody automatically keeps them in sync. You update one and forget the other, and now they disagree.
About the captures on this page. Every tcpdump sample here is a real capture, taken from a lab seat with a clean path to the internet (no intercepting middlebox on port 53; on a network that intercepts DNS, your captures lie to you). The DNS payload in each line is exactly as captured. The client address is rewritten to
192.0.2.45and the interface column toeth0, per the documentation-address rule (RFC 5737). The dnstap frames are built from the live frame schema with the same rewrite.
Step 1: find the parent's servers, then ask one#
The parent of isitdns.net is the .net zone itself. You do not have to memorise its servers, you ask DNS for them, the same way you would for any zone.
Client side:
dig @1.1.1.1 +short net. NSThe trailing dot matters: net. means "the TLD itself", not a hostname. Real output:
a.gtld-servers.net.
b.gtld-servers.net.
c.gtld-servers.net.
d.gtld-servers.net.Reading it: these are the .net zone's authoritative servers, and any one of them will do. The same trick works for every TLD: dig @1.1.1.1 +short com. NS for a .com name, dig @1.1.1.1 +short org. NS for .org, dig @1.1.1.1 +short io. NS for .io.
Now ask one of them what it holds for our zone:
dig @a.gtld-servers.net isitdns.net NS +norecurse@a.gtld-servers.net: one of the.netservers you just found.+norecurse: do not go and look it up, just tell me what you personally hold.
The parent does not consider itself authoritative for the child, so its answer arrives in the authority section rather than the answer section:
;; AUTHORITY SECTION:
isitdns.net. 172800 IN NS hera.ns.cloudflare.com.
isitdns.net. 172800 IN NS coleman.ns.cloudflare.com.Reading it: the parent believes two servers are in charge, and it hands out that list with a TTL of 172800 seconds, which is 48 hours. That long TTL is why delegation mistakes hurt: a wrong answer here can stay cached across the internet for two days.
This kind of response, pointing you somewhere else rather than answering, is called a referral.
The referral in Wireshark#
A referral has a recognisable shape: it is a response with an empty answer section and a populated authority section. That shape is filterable directly.
dns.flags.response == 1 and dns.count.answers == 0 and dns.count.auth_rr > 0The filtered view shows only referrals: every packet where a server said "not mine, ask them" instead of answering. Narrow it to one zone with and dns.qry.name == "isitdns.net". More DNS display filters, all verified against the field reference, in wireshark-dns-filters.
The referral in tcpdump#
Client side, on the machine running the dig:
sudo tcpdump -n -i any -c 2 host 192.5.6.30 and port 53192.5.6.30 is a.gtld-servers.net. Plain -n so nothing gets reverse-resolved mid-capture, -c 2 to stop after the query and its answer. No -v circus needed: the default one-line decode already names the qtype, the qname, and the section counts. Real capture of the dig above:
06:39:55.844553 eth0 Out IP 192.0.2.45.46887 > 192.5.6.30.53: 58315 [1au] NS? isitdns.net. (52)
06:39:55.876589 eth0 In IP 192.5.6.30.53 > 192.0.2.45.46887: 58315- 0/2/1 (98)Reading it: 58315 is the transaction ID, the same on both lines, which is how you pair them. NS? isitdns.net. is the question. On the response, 0/2/1 is the section count triplet, answer/authority/additional: zero answers, two authority records, one additional. That 0/2/1 shape IS the referral. The - suffix means recursion available is not set, which is correct: a TLD server does not recurse for you.
The referral in dnstap#
Server side. If you run the resolver, its dnstap feed records this same exchange as a RESOLVER_QUERY / RESOLVER_RESPONSE pair: your resolver walking to the parent on its own behalf. The response frame, decoded (schema is live, addresses are RFC 5737 documentation space):
{
"dnstap": { "operation": "RESOLVER_RESPONSE", "identity": "resolver-edge" },
"network": { "family": "IPv4", "protocol": "UDP",
"response-ip": "192.0.2.30", "response-port": 53 },
"dns": {
"id": 58315, "opcode": "QUERY", "rcode": "NOERROR",
"qname": "isitdns.net", "qtype": "NS",
"ancount": 0, "nscount": 2, "arcount": 1,
"flags": { "qr": true, "aa": false, "ra": false, "rd": false },
"resource-records": {
"an": [],
"ns": [
{ "name": "isitdns.net", "rdatatype": "NS", "class": "IN",
"ttl": 172800, "rdata": "coleman.ns.cloudflare.com." },
{ "name": "isitdns.net", "rdatatype": "NS", "class": "IN",
"ttl": 172800, "rdata": "hera.ns.cloudflare.com." }
],
"ar": []
}
}
}Same fingerprint as the tcpdump line: ancount 0, nscount 2, ra false, the NS set riding in ns[]. The frame carries what the wire carried, decoded and paired.
The syslog equivalent, a BIND-style query log line on the server that received the question (here, what the parent would log):
18-Aug-2026 06:39:55.844 queries: info: client @0x7f3a8c012345 192.0.2.45#46887 (isitdns.net): query: isitdns.net IN NS -E(0) (192.0.2.30)The - after the qtype means recursion desired was NOT set, which is your +norecurse arriving intact. A query log records the question only. The referral shape, 0 answers and 2 authority records, exists in the dnstap frame and on the wire, and nowhere in syslog. That gap is the reason this page shows both.
Step 2: ask the child#
Now ask one of those nameservers the same question.
Client side:
dig @coleman.ns.cloudflare.com isitdns.net NS +shortcoleman.ns.cloudflare.com.
hera.ns.cloudflare.com.Reading it: the child names the same two servers. This is the zone's own opinion about who runs it.
The child's answer in Wireshark#
The child, unlike the parent, IS authoritative for the zone, so its answer carries the aa flag. Filter for authoritative NS answers:
dns.qry.type == 2 and dns.flags.authoritative == 1The filtered view shows only NS answers from servers speaking as the authority: the child's own opinion, never the parent's referral (which has aa clear). Qtype 2 is NS; the full qtype table is in wireshark-dns-filters.
The child's answer in tcpdump#
Client side:
sudo tcpdump -n -i any -c 2 host 108.162.195.64 and port 53108.162.195.64 is one of coleman.ns.cloudflare.com's addresses; substitute the one your dig resolves. Real capture:
06:40:21.170321 eth0 Out IP 192.0.2.45.37913 > 108.162.195.64.53: 27043+ [1au] NS? isitdns.net. (52)
06:40:21.205069 eth0 In IP 108.162.195.64.53 > 192.0.2.45.37913: 27043*- 2/0/1 NS coleman.ns.cloudflare.com., NS hera.ns.cloudflare.com. (98)Reading it: the * on the response means the aa flag is set: this server answers as the authority, not from hearsay. Compare the shape against Step 1: the parent said 0/2/1 (a referral), the child says 2/0/1 (a real answer, two records in the answer section). Same question, opposite shape, and the difference between those two shapes is the entire subject of this page.
The child's answer in dnstap#
Server side. On the authority itself, this exchange is an AUTH_RESPONSE frame, and it carries two things nothing else on this page has: query-zone, the zone the server answered for, and aa true inside the message flags.
{
"dnstap": { "operation": "AUTH_RESPONSE", "identity": "ns-edge-1",
"query-zone": "isitdns.net." },
"network": { "family": "IPv4", "protocol": "UDP",
"query-ip": "192.0.2.45", "query-port": 37913 },
"dns": {
"id": 27043, "opcode": "QUERY", "rcode": "NOERROR",
"qname": "isitdns.net", "qtype": "NS",
"ancount": 2, "nscount": 0, "arcount": 1,
"flags": { "qr": true, "aa": true, "ra": false, "rd": true },
"resource-records": {
"an": [
{ "name": "isitdns.net", "rdatatype": "NS", "class": "IN",
"ttl": 86400, "rdata": "coleman.ns.cloudflare.com." },
{ "name": "isitdns.net", "rdatatype": "NS", "class": "IN",
"ttl": 86400, "rdata": "hera.ns.cloudflare.com." }
],
"ns": [], "ar": []
}
}
}And the query log line on the same box:
18-Aug-2026 06:40:21.170 queries: info: client @0x7f3a8c045678 192.0.2.45#37913 (isitdns.net): query: isitdns.net IN NS +E(0) (192.0.2.53)The + this time: the dig sent recursion desired (dig's default), and the authority ignored it, which is what ra false in the frame records.
Step 3: compare#
Put the two lists side by side. They should be identical, ignoring order.
| Result | Verdict |
|---|---|
| identical lists | healthy, nothing to do |
| parent lists a server the child does not | that server may not actually serve the zone, and resolvers sent there can fail |
| child lists a server the parent does not | that server is invisible to most of the internet, because resolvers follow the parent |
In our example both sides list coleman and hera, so this delegation is consistent.
Doing both steps at once, client side. +short prints the ANSWER section only, and a
referral carries the NS set in AUTHORITY, so the parent side needs +noall +authority
instead or it prints nothing at all:
echo "parent:"; dig @a.gtld-servers.net isitdns.net NS +norecurse +noall +authority
echo "child:"; dig @coleman.ns.cloudflare.com isitdns.net NS +shortThere is no separate wire decode for this step: the comparison happens in your head, between the Step 1 capture and the Step 2 capture. On the wire it is the 0/2/1 referral against the 2/0/1 authoritative answer, and the record lists inside them.
What a mismatch actually does to you#
Resolvers follow the parent's list when they first find their way to your zone. So:
- A nameserver the parent lists but that is not really serving your zone becomes a lame delegation, defined in RFC 9499 section 7 (DNS Terminology, BCP 219) as a nameserver that is delegated responsibility for a zone via NS records but is not actually serving it, quoting RFC 1912 section 2.8. The same entry records that current usage has drifted well past that: it now covers a nameserver that does not answer at all, one whose address is unreachable, and one that answers without the
aabit. RFC 9499 marks the term historic for exactly that reason and asks for the specific failure to be named instead. Some fraction of queries land on it and fail. Users see intermittent, unreproducible breakage, and whoever reports it is always the one person whose shoulder you cannot look over. - A nameserver the child lists but the parent does not is simply not used by most of the internet, no matter how correctly it is configured.
Both failure modes are intermittent, which is what makes them expensive. Nothing is fully down, so nothing gets escalated, and the symptom moves around.
Step 4: check each listed server actually answers#
A name on the list means nothing until the box behind it responds. Test every one.
Client side:
for ns in $(dig @a.gtld-servers.net isitdns.net NS +norecurse +noall +authority | awk '{print $5}'); do
echo "$ns -> $(dig @"$ns" isitdns.net SOA +norecurse +short | awk '{print $3}')"
doneThis walks the parent's list and asks each server for the zone's serial number. The parent's
list comes out of AUTHORITY, so it takes +noall +authority and an awk on the rdata field;
+short on that first query prints nothing and the loop silently runs zero times. The inner
query is a normal authoritative answer, so +short is right there.
What you want: every server prints the same number.
What the failures mean:
- blank or an error for one server: it is not answering for this zone. That is a lame delegation, and it is the thing to fix first.
- a different number: that server holds an older copy of the zone. Not lame, but stale. See zone transfers.
The serial sweep in Wireshark#
Two filters do the whole job. First, watch the sweep itself and read the serials straight off the packet list:
dns.qry.type == 6 and dns.flags.response == 1The filtered view shows every SOA answer; add a column on dns.soa.serial_number and the serial comparison becomes a glance down one column instead of four dig runs. Then hunt the failure:
dns.flags.rcode == 5The filtered view shows only REFUSED responses, which is the loudest way a listed server says "this zone is not mine". Rcode 5 is REFUSED; the full rcode table is in wireshark-dns-filters.
The lame server in tcpdump#
What a healthy server and a lame one look like, side by side. The healthy one first, a real capture of the SOA probe against a server that does hold the zone:
sudo tcpdump -n -i any -c 2 host 108.162.195.64 and port 5306:40:21.211620 eth0 Out IP 192.0.2.45.48983 > 108.162.195.64.53: 13107 [1au] SOA? isitdns.net. (52)
06:40:21.246670 eth0 In IP 108.162.195.64.53 > 192.0.2.45.48983: 13107*- 1/0/1 SOA (105)* again: authoritative, one SOA in the answer section. Now the lame shape. This capture is real too: the same server asked for a zone it does not serve, which is exactly what a resolver experiences when the parent's list points at the wrong box:
06:41:00.525320 eth0 Out IP 192.0.2.45.53937 > 108.162.195.64.53: 27701 [1au] SOA? google.com. (51)
06:41:00.559186 eth0 In IP 108.162.195.64.53 > 192.0.2.45.53937: 27701 Refused- 0/0/1 (45)Reading it: Refused, no *, and 0/0/1: no answer, no authority, nothing but the EDNS record. The server is up, reachable, and fast, and completely useless for this zone. That is what lame looks like: every layer below DNS is healthy, so nothing pages, and one query in N quietly dies here.
The lame server in dnstap and syslog#
Server side, on your own resolver. The lame answer comes back as a RESOLVER_RESPONSE frame with the rcode telling the story and every record array empty:
{
"dnstap": { "operation": "RESOLVER_RESPONSE", "identity": "resolver-edge" },
"network": { "family": "IPv4", "protocol": "UDP",
"response-ip": "192.0.2.10", "response-port": 53 },
"dns": {
"id": 44219, "opcode": "QUERY", "rcode": "REFUSED",
"qname": "isitdns.net", "qtype": "SOA",
"ancount": 0, "nscount": 0, "arcount": 1,
"flags": { "qr": true, "aa": false, "ra": false, "rd": false },
"resource-records": { "an": [], "ns": [], "ar": [] }
}
}BIND does not make you dig for this one: it has a dedicated log category for exactly this failure, named after it. The line it writes when a delegated server refuses the zone:
18-Aug-2026 06:41:00.559 lame-servers: info: error (unexpected RCODE REFUSED) resolving 'isitdns.net/SOA/IN': 192.0.2.10#53Grep a resolver's log for lame-servers and you get the internet's broken delegations as experienced from your seat, timestamped and attributed to the server that refused. That single log line is often the first evidence anyone has that Step 4 was never run.
Glue, and when you need it#
If a zone's nameservers are named inside the zone they serve, there is a chicken-and-egg problem. To find ns1.example.com you must ask example.com's nameservers, and ns1.example.com is one of them.
The parent solves it by publishing glue: the IP addresses alongside the referral, so a resolver can proceed. RFC 1034 §4.2.1 names these glue RRs and requires them exactly in this case, when the nameserver's name sits below the cut. Glue shows up in the additional section of the parent's response.
You only need glue when the nameserver names live inside the zone being delegated. isitdns.net uses *.ns.cloudflare.com nameservers, which live in a different zone that can be resolved independently, so no glue is required here.
Missing glue, when it is required, makes the zone unresolvable from a cold cache while looking perfectly correct in every control panel. In a capture, missing glue shows up as a referral whose additional section holds only the OPT record: the count triplet reads 0/2/1 where a glued referral to an in-zone nameserver reads 0/2/3 or more. The Wireshark filter dns.count.auth_rr > 0 and dns.count.add_rr <= 1 surfaces referrals carrying no glue; whether that is a problem depends on whether the NS names sit below the cut.
Quick reference#
The whole check, one block, client side:
echo "parent:"; dig @a.gtld-servers.net isitdns.net NS +norecurse +noall +authority
echo "child:"; dig @coleman.ns.cloudflare.com isitdns.net NS +short
echo "serials:"
for ns in $(dig @a.gtld-servers.net isitdns.net NS +norecurse +noall +authority | awk '{print $5}'); do
echo "$ns -> $(dig @"$ns" isitdns.net SOA +norecurse +short | awk '{print $3}')"
doneSwap in your zone, its TLD's servers, and its nameservers. Healthy is: both lists identical, every serial identical, nothing blank.
Every filter on this page, in one table. All field names verified against the Wireshark field reference; the rest of the DNS filter set is in wireshark-dns-filters.
| What you are hunting | Wireshark display filter |
|---|---|
| Referrals (the parent's handoff) | dns.flags.response == 1 and dns.count.answers == 0 and dns.count.auth_rr > 0 |
| One zone only | ... and dns.qry.name == "isitdns.net" |
| Authoritative NS answers (the child's opinion) | dns.qry.type == 2 and dns.flags.authoritative == 1 |
| SOA answers (the serial sweep) | dns.qry.type == 6 and dns.flags.response == 1 |
| REFUSED (the lame giveaway) | dns.flags.rcode == 5 |
| Referrals carrying no glue | dns.count.auth_rr > 0 and dns.count.add_rr <= 1 |
And the capture line that feeds all of them:
sudo tcpdump -n -i any -w delegation.pcap port 53Run the digs, stop the capture, open delegation.pcap in Wireshark, and every filter above works on it as-is.
See also#
- delegations: the concept in full
- check-an-auth: confirming a single authority is answering correctly
- wireshark-dns-filters: the full DNS capture and display filter set
- NS records
- SOA records
- dnsviz: a visual view of a delegation chain, including DNSSEC
- Zone transfers: why two authorities drift apart