rafaeljusto.github.io/dnstree
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.
Installing
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.
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
For an AI assistant
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.
Using it
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 |
|
Reverse lookups
-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.
$ 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
Exit codes
| 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.
The guide
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, what a name depends on, when its registration runs out, how
long a change takes to reach every cache, extended errors, NSID,
--software, cookies, --qmin, --without, --try-ns, --try-ds and
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, --pcap and --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.
How it walks
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.
Contributing
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.
Licence
MIT.