dany is a commandline DNS client that simulates (unreliable/semi-deprecated)
dns ANY queries by doing individual typed DNS queries and aggregating the
results. Queries are done concurrently for best performance.
dany [<types>] <hostname>
where <types> is a comma-separated list of DNS record types to query.
If unspecified, the default types list is: A,AAAA,HTTPS,MX,NS,SOA,TXT.
In the output, fields are separated by tabs, so you can use cut (with no
delimiter) to extract individual fields.
Examples:
$ dany cisco.com
A 72.163.4.185
AAAA 2001:420:1101:1::185
MX 10 alln-mx-01.cisco.com.
MX 20 rcdn-mx-01.cisco.com.
MX 30 aer-mx-01.cisco.com.
NS ns1.cisco.com.
NS ns2.cisco.com.
NS ns3.cisco.com.
SOA ns1.cisco.com. postmaster.cisco.com.
TXT 926723159-3188410
TXT MS=ms35724259
TXT docusign=5e18de8e-36d0-4a8e-8e88-b7803423fa2f
TXT docusign=95052c5f-a421-4594-9227-02ad2d86dfbe
TXT facebook-domain-verification=qr2nigspzrpa96j1nd9criovuuwino
TXT google-site-verification=K2w--6oeqrFjHfYtTsYyd2tFw7OQd6g5HJDC9UAI8Jk
TXT google-site-verification=PdOwpBvoBbr90361WK-DzUDRAwNMWd2f4jqgvGKlpWg
TXT google-site-verification=lW5eqPMJI4VrLc28YW-JBkqA-FDNVnhFCXQVDvFqZTo
TXT v=spf1 redirect=spfa._spf.cisco.com
TXT zpSH7Ye/seyY61hH8+Rq5Kb+ZJ9hDa+qeFBaD/6sPAAg+2POkGdP0byHb1pFVK9uZgYF2AIosUSZq4MB17oydQ==
$ dany mx,txt google.com
MX 10 aspmx.l.google.com.
MX 20 alt1.aspmx.l.google.com.
MX 30 alt2.aspmx.l.google.com.
MX 40 alt3.aspmx.l.google.com.
MX 50 alt4.aspmx.l.google.com.
TXT docusign=05958488-4752-4ef2-95eb-aa7ba8a3bd0e
TXT facebook-domain-verification=22rm551cu4k0ab0bxsw536tlds4h95
TXT globalsign-smime-dv=CDYX+XFHUw2wml6/Gb8+59BsH31KzUr6c1l2BPvqKX8=
TXT v=spf1 include:_spf.google.com ~all
The -w/--www flag additionally probes the www.<hostname> label. By default
only A and AAAA are queried for the www label; if -t/--types (or -a/--all)
is given explicitly, that set is used for the www probe too. With -T/--tag,
apex and www results are emitted on separate lines tagged with their
hostname; without it, they share the same output and identical rows are
collapsed (so a www that resolves to the same IP as the apex won't appear
twice).
$ dany -w -T google.com
google.com A 172.217.25.110
google.com AAAA 2404:6800:4006:80d::200e
google.com MX 10 smtp.google.com.
google.com NS ns1.google.com.
google.com NS ns2.google.com.
google.com NS ns3.google.com.
google.com NS ns4.google.com.
google.com SOA ns1.google.com. dns-admin.google.com.
google.com TXT v=spf1 include:_spf.google.com ~all
www.google.com A 142.251.150.119
www.google.com AAAA 2404:6800:4006:80d::2004
Internationalized domain names are supported: hostnames are normalized to their
IDNA A-label (punycode) form before querying, so you can pass a UTF-8 name
directly (dany münchen.de) or its already-encoded equivalent
(dany xn--mnchen-3ya.de) — both resolve identically. Names are case-folded and
Unicode-normalized (NFC) in the process; underscore labels (_dmarc.<domain>,
--usd probes) are preserved. This applies to both dany and dnx.
The -f/--fmt flag selects the output format: text (default, the
tab-separated form shown above), json, or yaml (yml as a convenience
alias). -j/--json is a shorthand for --fmt json; combining it with a
different --fmt is an error rather than a silent override. In json and
yaml modes:
- Each record carries both
rdata(the canonical DNS presentation form, always a string) anddata(a typed per-RR-type payload —addressfor A/AAAA,preference+exchangefor MX, full RFC field set for SOA,stringsarray for TXT, etc.). - A
schema_version: 1envelope wraps the per-hostname output. Multi- hostname runs emit NDJSON (one object per line) injsonmode and multi-document YAML (----separated) inyamlmode. - Errors fold into the envelope's
errorsfield with a stable machine- readablecode(NXDOMAIN,SERVFAIL,EXCHANGE_ERROR, …) — nothing goes to stderr. - PTR records (when
-p/--ptris set) appear as standalone entries with the source IP carried indata.ipfor easy joining (text mode folds them into the matching A/AAAA row instead).
$ dany -f json -t a,mx example.com
{"schema_version":1,"query":{"hostname":"example.com","types":["a","mx"],
"server":"8.8.8.8:53","options":{"www":false,"usd":false,"ptr":false}},
"answers":[{"type":"A","name":"example.com.","ttl":86400,"class":"IN",
"rdata":"96.7.128.198","data":{"address":"96.7.128.198"}}],"errors":[]}
$ dany -f yaml -t mx example.com
---
schema_version: 1
query:
hostname: example.com
types:
- mx
server: 8.8.8.8:53
options:
www: false
usd: false
ptr: false
answers:
- type: MX
name: example.com.
ttl: 86400
class: IN
rdata: 10 mx.example.com.
data:
preference: 10
exchange: mx.example.com.
errors: []
dnx is a companion CLI that reads one or more hostnames (as arguments or on
stdin) and reports those that return NXDOMAIN. For safety it probes multiple
record types concurrently (MX,NS,SOA by default) and only reports a hostname
as NXDOMAIN if every type returns NXDOMAIN — a host that answers any one type
is not reported.
dnx <hostname> [<hostname> ...]
dnx < hostnames.txt
Examples:
# Report which of these domains don't exist
$ dnx example.com does-not-exist.example nxdomain.test
does-not-exist.example
nxdomain.test
# Feed a list on stdin
$ cat domains.txt | dnx
...
# -c/--count: report every hostname with its count of non-NXDOMAIN responses
$ dnx -c example.com does-not-exist.example
example.com,3
does-not-exist.example,0
# -V/--invert: report the hostnames that DO resolve (not NXDOMAIN)
$ dnx -V example.com does-not-exist.example
example.com
Useful flags:
-s/--server <ip>— use a single resolver IP (overrides the system resolvers).-r/--resolv <file>— load resolver IPs from a file (one per line); queries rotate round-robin across them.-t/--types <list>— override the NX-probe types (defaultMX,NS,SOA).-C/--concurrency <n>— hostnames queried concurrently per resolver (default3; the effective cap isn × number-of-resolvers).-c/--count,-V/--invert— as shown above.--version— print version and exit.
Both CLIs are thin wrappers over the dany package, which you can import
directly:
go get github.com/gavincarr/dany
package main
import (
"fmt"
"github.com/gavincarr/dany"
)
func main() {
// Build a resolver set (round-robins across all of them per query).
resolvers, err := dany.NewResolversFromStrings([]string{"1.1.1.1", "8.8.8.8"})
if err != nil {
panic(err)
}
q := &dany.Query{
Hostname: "example.com",
Types: dany.DefaultRRTypes, // or e.g. []string{"A", "MX", "TXT"}
Resolvers: resolvers,
}
answers, errs := dany.RunQuery(q)
for _, e := range errs {
fmt.Println("error:", e) // *dany.QueryError; errors.Is(e, dany.ErrNXDomain) works
}
fmt.Print(dany.Render(answers, false)) // canonical tab-separated text
// ...or structured, sharing one typed envelope:
// dany.RenderJSON(answers, q, errs)
// dany.RenderYAML(answers, q, errs)
}Key API surface:
- Query functions.
RunQuery(q *Query) ([]Answer, []error)does the typed-ANY aggregation;RunNXQuery(q *Query) intpowersdnx(returns the number of non-NXDOMAIN responses, so0means every probe type was NXDOMAIN). Both normalizeq.Hostnameto its IDNA A-label (punycode) form first, so you can set a UTF-8Hostname("münchen.de") directly; the caller's*Queryis left unmutated.RunQueryreports a name IDNA rejects as a single*QueryErrorwithCode"INVALID_NAME"and fires no lookups. - Renderers.
Render(answers, tagHostname)produces the tab-separated text;RenderJSON/RenderYAMLproduce the structured formats. All three consume the same[]Answer— the query path does no formatting and the renderers do no I/O, so you can also consume[]Answerdirectly and skip them. - Resolvers.
NewResolvers(ips ...net.IP)builds a round-robin set from one or more parsed IPs (panics if given none);NewResolversFromStrings([]string)parses and validates IP strings, returning an error instead;LoadResolvers(file)reads them from a file. Append more with(*Resolvers).Append. - Errors are structured. Each is a
*QueryErrorcarrying a stableCode(NXDOMAIN,SERVFAIL,EXCHANGE_ERROR,UNSUPPORTED_TYPE,INVALID_NAME, …) and supportserrors.IsagainstErrNXDomain/ErrServFail. - Type constants.
DefaultRRTypes,SupportedRRTypes,DNSSECRRTypes,DNSSECBundle,NXTypes, andSupportedUSDsexpose the same type sets the CLIs use.
Note that Answer.RR is a github.com/miekg/dns RR, so inspecting records
directly couples you to that package (going through the renderers does not).
Gavin Carr gavin@openfusion.net
MIT. See LICENCE.