Resolve a name the way a resolver does — from the root servers down, following every referral — and draw the path it took.
$ dnstree www.example.com A
. (root)
├── a.root-servers.net. 198.41.0.4 AS19836 241ms NOERROR referral → com.
│ ├── l.gtld-servers.net. 192.41.162.30 AS19836 247ms NOERROR referral → example.com.
│ │ ├── hera.ns.cloudflare.com. 108.162.192.162 AS13335 233ms NOERROR AA
│ │ │ ├── www.example.com. 300 A 172.66.147.243
│ │ │ └── www.example.com. 300 A 104.20.23.154
│ │ ├── hera.ns.cloudflare.com. 172.64.32.162 (not queried)
│ │ ├── hera.ns.cloudflare.com. 173.245.58.162 (not queried)
│ │ ├── hera.ns.cloudflare.com. 2606:4700:50::adf5:3aa2 (not queried)
│ │ └── (and 8 more not queried)
│ ├── l.gtld-servers.net. 2001:500:d937::30 (not queried)
│ ├── j.gtld-servers.net. 192.48.79.30 (not queried)
│ ├── j.gtld-servers.net. 2001:502:7094::30 (not queried)
│ └── (and 22 more not queried)
├── a.root-servers.net. 2001:503:ba3e::2:30 (not queried)
├── b.root-servers.net. 170.247.170.2 (not queried)
├── b.root-servers.net. 2801:1b8:10::b (not queried)
└── (and 22 more not queried)
✔ answered in 722ms · resolver in 243ms · 3 queries · 3 servers
dig +trace tells you the same story in prose. dnstree draws it, and says
what it found on the way: which servers were lame, which delegations are
broken, how long each hop took, which AS announces each address, and whether
the chain of trust holds.
go install github.com/rafaeljusto/dnstree/v2/cmd/dnstree@latest
Or run it straight from a container:
docker run --rm ghcr.io/rafaeljusto/dnstree www.example.com A
dnstree-web, the same walk behind a form, has an image of its own:
docker run --rm -p 8080:8080 ghcr.io/rafaeljusto/dnstree-web
Every release also carries a
binary for macOS, Linux, FreeBSD and Windows, a Debian, RPM and Alpine package
for amd64, arm64 and 32-bit arm, and a Homebrew formula. The packages install a man
page: man dnstree. checksums.txt covers every file in the release.
packaging/ says how they are built.
Installing a downloaded package
The release notes give the command for each, with the version in place of
X.Y.Z:
# Debian, Ubuntu
sudo dpkg -i dnstree_X.Y.Z_amd64.deb
# Fedora, RHEL
sudo rpm -i dnstree-X.Y.Z-1.x86_64.rpm
# Alpine
sudo apk add --allow-untrusted dnstree_X.Y.Z_x86_64.apk
# Homebrew
brew install --formula ./dnstree.rb
A skill teaches a model which flags answer which question, how to read the exit codes, and what not to run without asking. In Claude Code:
/plugin marketplace add rafaeljusto/dnstree
/plugin install dnstree@dnstree
Other agents that read SKILL.md folders can copy
packaging/plugin/skills/dnstree/ into
their skills directory. Either way, dnstree itself has to be installed.
dnstree [flags] NAME [TYPE...]
TYPE defaults to A. Give several and each is walked in turn, from the root
servers down; --names
reads a list of them from a file. A type names one RRset, so ANY and the zone transfers
AXFR and IXFR are refused: servers answer ANY with a sample of their
choosing (RFC 8482), and a walk would draw that as the whole answer.
A name in any script is asked in punycode, which is how the DNS holds it:
dnstree münchen.de walks to xn--mnchen-3ya.de, and draws it that way.
| Flag | What it does |
|---|---|
-x |
resolve the PTR of this address, in place of a name and a type, and say whether its names point back to it |
--names |
walk every NAME [TYPE...] line of a file, or of - for the standard input, one after another; json writes a line per walk, openmetrics one file over all of them |
-4, -6 |
ask only IPv4 or only IPv6 servers; the others are drawn unqueried |
--udp, --tcp |
carry the queries over plain DNS (--udp is the default) |
--dot, --doh |
carry them encrypted, over TLS or HTTPS |
--fallback |
let plain DNS pick up a hop the transport could not |
--all |
ask every nameserver of a zone, not just the first that answers |
--dnssec |
ask for signatures and follow the chain of trust |
--check-ns |
ask the zone that answered for its own NS set, its nameservers' addresses and its CSYNC, and compare them with the delegation and its glue; ask for the referral again over TCP to catch glue it left out; ask the parent where it takes notice of a change (DSYNC) |
--check-ds |
ask the zone for its CDS and CDNSKEY and compare them with the parent's DS, or, for a zone with none, check the bootstrap signals of RFC 9615; ask the parent where it takes notice of a change (DSYNC) |
--report |
tell the agent a zone names that its chain of trust is bogus (RFC 9567) |
--serial |
ask every nameserver of the zone which copy of it they serve, and compare, and judge the SOA's timers; every hop also asks which copy its answer came from (RFC 9660) |
--check-axfr |
ask every nameserver of the zone for the whole of it, as a stranger, and say which hand it over, and whether its denials (NSEC) list it anyway |
--check-recursion |
ask every nameserver of the zone to look up somebody else's name, and say which do |
--check-edns |
ask every nameserver of the zone the RFC 8906 EDNS tests, the same in mixed case and over TCP, and say which fail them |
--caa |
say which certificate authorities may issue for the name, and which CAA set decides it (RFC 8659) |
--spf |
draw the name's SPF policy as the tree of lookups a mail server makes, and count them against the limit of ten (RFC 7208) |
--mail |
check the name's MX hosts the way a sender that checks DANE does, and its MTA-STS, TLS-RPT and DMARC records (RFC 7672) |
--tlsa |
connect to each MX host DANE covers, start TLS, and match the certificate it presents against its TLSA set; needs --mail and --dnssec |
--dkim SELECTOR |
check the DKIM key the name publishes for a selector: there, parses, long enough, not revoked or SHA-1 only (RFC 6376, RFC 8301); repeat it; needs --mail |
--svcb |
follow the name's HTTPS or SVCB records to the servers they name, and hold their address hints against what the servers resolve to (RFC 9460) |
--srv |
follow a service's SRV records to the hosts they name, and say which have no address, are aliases, or sit where no client can reach them (RFC 2782) |
--deps |
list every zone the name depends on: the walk's own, and those the lookups of every nameserver of each go through in turn, with which are unsigned |
--rdap |
ask the domain's registry when the registration runs out and whether it is held, and compare its nameservers and DS with the TLD's (RFC 9083) |
--propagation |
say how long each kind of change to the zone takes to reach every cache, from the TTLs the walk saw |
--rollover |
say which step of a key rollover the zone is at, what comes next, and the TTL it waits on (RFC 7583); needs --dnssec, or a walk saved with it |
--check |
run the checks that grade a zone together, and grade its health one area at a time: passed, worth a look, broken or skipped |
--nsid |
ask each server which of itself answered, and draw it beside the address |
--software |
ask each server which software it runs (version.bind), and draw what it says beside the address |
--repeat N |
ask each hop's question N more times (at most 100), and say how many came back and how long they took |
--cookie |
send each server a DNS cookie (RFC 7873), and say how it answered |
--qmin |
ask each zone for no more of the name than it needs, the way resolvers do (RFC 9156) |
--without |
walk as though this nameserver, address or prefix were down, and see whether the name still answers; repeat it |
--try-ns |
walk as though ZONE were already delegated to SERVER, before the registry is told; repeat it for each new nameserver |
--compare-ns |
ask each question of ZONE's servers and of the new ones named, and say per question what differs, TTLs included; with --names, a whole list; repeat it for each new nameserver |
--try-ds |
walk as though the parent of ZONE already held this DS, and say which key it should point at when it does not hold; repeat it for a set; needs --dnssec |
--subnet |
ask as though from this client subnet, and say what each server made of it |
--no-asn |
skip the origin AS lookups |
--no-compare |
skip the question put to a recursive resolver, and the comparison with it |
--ddr |
ask each resolver which encrypted resolvers stand for it (RFC 9462), and say what they offer; with --check-resolver, also what each says of itself (RFC 9606), held against what it does |
--check-resolver |
see whether each resolver validates DNSSEC, trusts the root's keys, rewrites NXDOMAIN, sends your subnet on (ECS), minimises queries and makes up IPv6 addresses (DNS64), and which network its questions reach zones from |
--format |
tree (the default), ascii, emoji, waterfall, waterfall-ascii, waterfall-mermaid, markdown, teach, json, dot, mermaid, openmetrics, dig, web or web-3d |
--web-addr, --no-browser |
where --format web and web-3d serve the page, and whether a browser is opened at it |
--live |
draw the tree as the walk makes it, hop by hop |
--watch |
walk again this often, and say only what changed since the walk before |
--explain |
say in sentences what the walk came to, under the tree |
--diff |
say what has changed since the last walk of the same question |
--expect |
require this of the walk, and exit 4 where it does not hold; repeat it |
--from |
draw a walk --format json saved, from a file or -, instead of making one |
--against |
say how the walk differs from one --format json saved, from a file or - |
--pcap |
save the walk's queries and answers as a packet capture, for Wireshark or tcpdump -r |
--replay |
walk again offline, every server answering from a capture --pcap made |
--color |
auto (the default: only on a terminal, and off where NO_COLOR is set or TERM is unset or dumb), always or never |
--timeout, --retries |
how long one query may take (2s), and how often to ask again after a silence (once) |
--max-depth, --max-queries, --max-cname |
the budgets that keep a walk finite: 16 zone cuts, 64 queries (256 with --all, 512 with --check or --deps), 8 aliases |
--port |
the port nameservers are asked on (53; 853 with --dot, 443 with --doh) |
--root-hints |
where the walk starts, instead of the built-in hints |
--trust-anchors |
the DS records to trust, instead of the built-in ones |
--root |
one server to start from, instead of a hints file; repeat it for more |
--resolver |
a recursive server to use, instead of the host's own; repeat it to ask several |
--tls-ca, --tls-insecure |
how --dot and --doh verify a server, or that they do not |
--config, --no-config |
take the defaults from this file, or from no file at all |
--debug |
report every hop on stderr as it is made |
--schema |
print the JSON Schema of --format json and stop |
--version |
print the version and stop |
-x ADDR asks for the PTR record of an address, the way dig -x does:
192.0.2.1 is asked as 1.2.0.192.in-addr.arpa., and an IPv6 address nibble
by nibble under ip6.arpa.. It takes the place of the name and the type. The
reverse tree is delegated like any other, only more deeply, and its
nameservers are usually named somewhere else entirely, so a reverse walk is
where the side walks that resolve a nameserver's own name are most often seen.
A delegation below a /24 (RFC 2317) arrives as an alias, and is followed.
Each name the PTR set gives is then looked up for an address of the same
family, from the root, to say whether it points back: forward-confirmed reverse
DNS, which mail receivers judge a sending server by. Those lookups are drawn
as asides and never change the exit code; a ptr: line under the tree says
which names point back, and which do not and why.
The walk to the PTR of 8.8.8.8
$ dnstree -x 8.8.8.8 --no-asn
. (root)
├── a.root-servers.net. 198.41.0.4 235ms NOERROR referral → in-addr.arpa.
│ ├── f.in-addr-servers.arpa. 193.0.9.1 227ms NOERROR referral → 8.in-addr.arpa.
│ │ ├── . (root) (resolving u.arin.net.)
│ │ │ ├── a.root-servers.net. 198.41.0.4 234ms NOERROR referral → net.
│ │ │ │ ├── m.gtld-servers.net. 192.55.83.30 238ms NOERROR referral → arin.net.
│ │ │ │ │ ├── ns1.arin.net. 199.212.0.108 306ms NOERROR AA
│ │ │ │ │ │ └── u.arin.net. 43200 A 204.61.216.50
│ │ │ │ │ ├── ns1.arin.net. 2001:500:13::108 (not queried)
│ │ │ │ │ ├── ns2.arin.net. 199.71.0.108 (not queried)
│ │ │ │ │ ├── ns2.arin.net. 2001:500:31::108 (not queried)
│ │ │ │ │ └── (and 4 more not queried)
│ │ │ │ ├── m.gtld-servers.net. 2001:501:b1f9::30 (not queried)
│ │ │ │ ├── k.gtld-servers.net. 192.52.178.30 (not queried)
│ │ │ │ ├── k.gtld-servers.net. 2001:503:d2d::30 (not queried)
│ │ │ │ └── (and 22 more not queried)
│ │ │ ├── a.root-servers.net. 2001:503:ba3e::2:30 (not queried)
│ │ │ ├── b.root-servers.net. 170.247.170.2 (not queried)
│ │ │ ├── b.root-servers.net. 2801:1b8:10::b (not queried)
│ │ │ └── (and 22 more not queried)
│ │ └── u.arin.net. 204.61.216.50 219ms NOERROR referral → 8.8.8.in-addr.arpa.
│ │ ├── . (root) (resolving ns3.google.com.)
│ │ │ ├── a.root-servers.net. 198.41.0.4 235ms NOERROR referral → com.
│ │ │ │ ├── l.gtld-servers.net. 192.41.162.30 236ms NOERROR referral → google.com.
│ │ │ │ │ ├── ns2.google.com. 2001:4860:4802:34::a 57ms NOERROR AA
│ │ │ │ │ │ └── ns3.google.com. 345600 A 216.239.36.10
│ │ │ │ │ ├── ns2.google.com. 216.239.34.10 (not queried)
│ │ │ │ │ ├── ns1.google.com. 2001:4860:4802:32::a (not queried)
│ │ │ │ │ ├── ns1.google.com. 216.239.32.10 (not queried)
│ │ │ │ │ └── (and 4 more not queried)
│ │ │ │ ├── l.gtld-servers.net. 2001:500:d937::30 (not queried)
│ │ │ │ ├── j.gtld-servers.net. 192.48.79.30 (not queried)
│ │ │ │ ├── j.gtld-servers.net. 2001:502:7094::30 (not queried)
│ │ │ │ └── (and 22 more not queried)
│ │ │ ├── a.root-servers.net. 2001:503:ba3e::2:30 (not queried)
│ │ │ ├── b.root-servers.net. 170.247.170.2 (not queried)
│ │ │ ├── b.root-servers.net. 2801:1b8:10::b (not queried)
│ │ │ └── (and 22 more not queried)
│ │ └── ns3.google.com. 216.239.36.10 237ms NOERROR AA
│ │ ├── 8.8.8.8.in-addr.arpa. 86400 PTR dns.google.
│ │ └── . (root) (A of dns.google. for the reverse check)
│ │ ├── a.root-servers.net. 198.41.0.4 234ms NOERROR referral → google.
│ │ │ ├── ns-tld2.charlestonroadregistry.com. 216.239.34.105 240ms NOERROR referral → dns.google.
│ │ │ │ ├── ns2.zdns.google. 216.239.34.114 245ms NOERROR AA
│ │ │ │ │ ├── dns.google. 900 A 8.8.8.8
│ │ │ │ │ └── dns.google. 900 A 8.8.4.4
│ │ │ │ ├── ns2.zdns.google. 2001:4860:4802:34::72 (not queried)
│ │ │ │ ├── ns3.zdns.google. 216.239.36.114 (not queried)
│ │ │ │ ├── ns3.zdns.google. 2001:4860:4802:36::72 (not queried)
│ │ │ │ └── (and 4 more not queried)
│ │ │ ├── ns-tld2.charlestonroadregistry.com. 2001:4860:4802:34::69 (not queried)
│ │ │ ├── ns-tld4.charlestonroadregistry.com. 216.239.38.105 (not queried)
│ │ │ ├── ns-tld4.charlestonroadregistry.com. 2001:4860:4802:38::69 (not queried)
│ │ │ └── (and 6 more not queried)
│ │ ├── a.root-servers.net. 2001:503:ba3e::2:30 (not queried)
│ │ ├── b.root-servers.net. 170.247.170.2 (not queried)
│ │ ├── b.root-servers.net. 2801:1b8:10::b (not queried)
│ │ └── (and 22 more not queried)
│ ├── f.in-addr-servers.arpa. 2a13:27c0:30::1 (not queried)
│ ├── b.in-addr-servers.arpa. 199.253.183.183 (not queried)
│ ├── b.in-addr-servers.arpa. 2001:500:87::87 (not queried)
│ └── (and 8 more not queried)
├── a.root-servers.net. 2001:503:ba3e::2:30 (not queried)
├── b.root-servers.net. 170.247.170.2 (not queried)
├── b.root-servers.net. 2801:1b8:10::b (not queried)
└── (and 22 more not queried)
ptr: dns.google. points back to 8.8.8.8
✔ answered in 2.9s · resolver in 222ms · 13 queries · 10 servers
| Code | Meaning |
|---|---|
| 0 | something answered |
| 1 | the command line, a file it names, or the address --format web or web-3d serves on could not be used |
| 2 | the walk ended without an answer |
| 3 | the chain of trust is broken |
| 4 | an expectation given with --expect was not met |
The tree, the summary, --explain and the address --format web serves on go
to stdout; errors, --debug, an unmet expectation, a cache --diff could not
use, a capture --pcap could not write and a report --report could not send
go to stderr. -h prints
the usage on stderr and exits 1.
Each feature has a section of its own in docs/guide/, with real output:
- Configuring: the file of defaults, pointing a walk at servers other than the real root, and the transports.
- DNSSEC: the chain of trust,
--check-ds,--rollover,--report, and what DNSSEC is worth to ECH. - Checking a zone:
--check, parent and child NS sets, serials and SOA timers, zone transfers and open recursion, how they handle EDNS, aliases where they may not be, addresses nobody can reach, who may issue certificates, SPF, DANE, DMARC and DKIM, which servers a browser connects to, where a service's SRV records lead, what a name depends on, when its registration runs out, how long a change takes to reach every cache, extended errors, NSID,--software,--repeat, cookies,--qmin,--without,--try-ns,--try-ds,--compare-nsand answer sizes. - Resolvers: how the walk compares with the resolvers people use, why one answers SERVFAIL, from here or from another subnet, whether they offer encryption, and how each behaves.
- Reading a walk:
--explain,--live, the waterfall, every other format,--schema,--pcapand--replay. - Scripts and monitoring:
--expect,--diff, several questions and--names,--watch, saved walks and OpenMetrics. - dnstree-web: the same walk behind a form, as a service.
Every hop is a question to one server, and every answer is classified before anything is followed:
- A referral is NOERROR with the AA bit clear, an empty answer, and one NS RRset whose owner sits strictly below the zone that was asked and covers the name being chased. Because a referral only ever goes down, a zone cannot repeat, and the walk cannot circle.
- Glue is taken only from a server entitled to give it: addresses at or
below the zone that server holds. That is how the root can hand out the
addresses of the gTLD servers while a
com.server cannot vouch for a name in.net. A nameserver named elsewhere with no usable address is found with a walk of its own, drawn as a branch. - A nameserver inside the zone it serves, with no glue, cannot be reached by anything: the delegation is broken, and the walk says so rather than looping.
- A nameserver handed out at an address nobody on the internet can reach, a private, loopback or documentation one, is asked anyway and marked, with a warning that says where the address came from and who can fix it (guide).
- Aliases start again from the root for the target, bounded and cycle checked. Truncation brings the answer back over TCP, and a server that cannot parse EDNS0 is asked again without it; both stay on the hop that needed them.
- Query, depth and alias budgets bound every walk, so the tool always terminates and always draws what it learned.
The root hints and the DNSSEC trust anchors are embedded in the binary.
make roothints refreshes them, verifying ICANN's signature over the anchors
before believing a byte.
Issues and pull requests are welcome. CONTRIBUTING.md covers
how to get a change reviewed, how the tests work, the make targets, and why
your commit subject decides the next version number. Everyone taking part is
expected to follow the code of conduct.
Found a security problem? Please do not open an issue: the security policy says where to send it.
MIT.
