ZDNS 是一个高速 DNS 解析器和命令行工具,用于执行大规模 DNS 测量。ZDNS 使用 Go 语言编写,包含自己的递归解析代码和一个针对多样化名称查询优化的缓存。我们使用 https://github.com/zmap/dns 来构建和解析原始 DNS 数据包。有关 ZDNS 架构和性能的更多信息,请参阅以下发表于 ACM 互联网测量会议 '22 的论文。
[!TIP] ZDNS Wiki 包含有关 ZDNS 的更多信息,并介绍了用例和示例。
可以通过克隆仓库并运行 make install 来安装 ZDNS。
git clone https://github.com/zmap/zdns.git
cd zdns
make install
该库由一个 ResolverConfig 结构体组成,该结构体包含所有查找操作的配置选项。
ResolverConfig 用于创建 1 个或多个 Resolver 结构体,这些结构体将执行所有查找。一个 Resolver
每次只能执行一次查找(它不是线程安全的),为了实现并行,应使用多个 Resolver 结构体。
请参阅我们的示例了解如何使用该库。模块用于定义查找的行为。
ZDNS 提供了几种类型的模块:
原始 DNS 模块:提供来自服务器的原始 DNS 响应,格式类似 dig,但为 JSON。几乎每种 DNS 记录类型都有对应的模块。
查找模块:在需要多次查询时提供更友好的响应(例如,在 NSLOOKUP 中接收到 NS 记录后,完成对 IP 地址的额外 A 查找)。
其他模块:提供其他附加的查询服务器方式(例如,bind.version)。
下面详细介绍这些模块:
A、AAAA、AFSDB、ANY、ATMA、AVC、AXFR、BINDVERSION、CAA、CDNSKEY、CDS、CERT、 CNAME、CSYNC、DHCID、DMARC、DNSKEY、DS、EID、EUI48、EUI64、GID、GPOS、HINFO、 HIP、HTTPS、ISDN、KEY、KX、L32、L64、LOC、LP、MB、MD、MF、MG、MR、MX、NAPTR、 NID、NINFO、NS、NSAPPTR、NSEC、NSEC3、NSEC3PARAM、NSLOOKUP、NULL、NXT、 OPENPGPKEY、PTR、PX、RP、RRSIG、RT、SVCBS、MIMEA、SOA、SPF、SRV、SSHFP、TALINK、 TKEY、TLSA、TXT、UID、UINFO、UNSPEC 和 URI 模块以 JSON 形式提供原始 DNS 响应,类似于 dig。
例如,命令:
echo "censys.io" | zdns A
返回:
{
"name": "censys.io",
"results": {
"A": {
"data": {
"additionals": [
{
"flags": "",
"type": "EDNS0",
"udpsize": 512,
"version": 0
}
],
"answers": [
{
"answer": "104.18.10.85",
"class": "IN",
"name": "censys.io",
"ttl": 300,
"type": "A"
},
{
"answer": "104.18.11.85",
"class": "IN",
"name": "censys.io",
"ttl": 300,
"type": "A"
}
],
"protocol": "udp",
"resolver": "[2603:6013:9d00:3302::1]:53"
},
"duration": 0.285295416,
"status": "NOERROR",
"timestamp": "2024-08-23T13:12:43-04:00"
}
}
}
原始 DNS 响应通常不提供您 想要 的数据。例如,MX 响应可能不在附加部分中包含关联的 A 记录,从而需要额外的查找。为了解决这个问题并提供更友好的接口,我们还提供了几个 查找 模块:alookup、mxlookup 和 nslookup。
alookup 类似 nslookup,会跟随 CNAME 记录。
mxlookup 会额外对与交换记录对应的 IP 地址执行 A 查找。
nslookup 会额外对与 NS 记录对应的 IP 地址执行 A/AAAA 查找。
例如,
echo "censys.io" | zdns mxlookup --ipv4-lookup
返回:
{
"name": "censys.io",
"results": {
"MXLOOKUP": {
"data": {
"exchanges": [
{
"class": "IN",
"ipv4_addresses": [
"209.85.202.27"
],
"name": "alt1.aspmx.l.google.com",
"preference": 5,
"ttl": 300,
"type": "MX"
},
{
"class": "IN",
"ipv4_addresses": [
"142.250.31.26"
],
"name": "aspmx.l.google.com",
"preference": 1,
"ttl": 300,
"type": "MX"
}
]
},
"duration": 0.154786958,
"status": "NOERROR",
"timestamp": "2024-08-23T13:10:11-04:00"
}
}
}
ZDNS 还支持特殊的“调试”DNS 查询。模块包括:BINDVERSION。
ZDNS 支持根据所需行为以多种格式提供输入。
最基本的输入是按换行符分隔的名称列表。例如:
从标准输入:
echo "google.com\nyahoo.com" | zdns A
cat list_of_domains.txt | zdns A
从文件
zdns A --input-file=list_of_domains.txt
如果不需要解析很多域名,可以将域名作为 CLI 参数提供,类似 dig,以便于使用。
例如:
zdns A google.com --name-servers=1.1.1.1
等价于 dig -t A google.com @1.1.1.1
通常,ZDNS 会从 --name-servers 中为每个域名查找随机选择一个名称服务器。如果希望为每个域名指定不同的名称服务器,可以通过提供由换行符分隔的 domainName,nameServerIP 对来实现。这将覆盖使用 --name-servers 提供的任何名称服务器。
例如:
echo "google.com,1.1.1.1\nfacebook.com,8.8.8.8" | zdns A
您可以在输出中看到每个域名的 resolver 与指定的一致(附加和答案部分已省略以保持简洁):
$ echo "google.com,1.1.1.1\nfacebook.com,8.8.8.8" | zdns A
{"name":"google.com","results":{"A":{"data":{"additionals":...,"answers":[...],"protocol":"udp","resolver":"1.1.1.1:53"},"duration":0.030490042,"status":"NOERROR","timestamp":"2024-09-13T09:51:34-04:00"}}}
{"name":"facebook.com","results":{"A":{"data":{"additionals":[...],"answers":[...],"protocol":"udp","resolver":"8.8.8.8:53"},"duration":0.061365459,"status":"NOERROR","timestamp":"2024-09-13T09:51:34-04:00"}}}
区域文件(来自 ICANN CZDS 或类似来源)可以使用 --zone-file 标志作为 ZDNS 的输入源。这允许从标准输入(默认)或使用 --input-file 标志从文件解析区域文件。
默认情况下,ZDNS 仅从每个区域文件记录中提取名称。如果还想解析记录类型(如 CNAME 或 NS 记录)答案部分中引用的名称,可以使用 --zone-file-include-targets CLI 标志。
例如,使用 --zone-file-include-targets 和以下 DNS 区域文件条目:
example.com. 3600 IN NS ns1.example.com
example.com 和 ns1.example.com 都将被解析。
ZDNS 还支持为每个输入行传递“触发器”,将输入行映射到特定模块。通过此功能,可以指定某些域名用特定模块进行查找。
输入格式为:
domain_name,name_server,trigger,trigger_2,etc,其中 nameServer 可以为空以使用默认名称服务器,并且可以指定 1 个或多个触发器。
示例 input.csv 文件:
example.com,,a-trigger
google.com,,a-trigger,cname-trigger
example.com,1.1.1.1,aaaa-trigger
yahoo.com
apnews.com,1.1.1.1
以及相应的 multiple.ini:
; Specify Global Options here
[Application Options]
iterative=true
prefer-ipv6-iteration="true"
; List out modules and their respective module-specific options here. A module can only be listed once
[A]
trigger = "a-trigger"
[AAAA]
trigger = "aaaa-trigger"
[CNAME]
trigger = "cname-trigger"
这将查找:
example.com 使用 A 模块,默认名称服务器google.com 使用 A + CNAME 模块,默认名称服务器example.com 使用 AAAA 模块,Cloudflare 的 1.1.1.1 解析器yahoo.com 使用所有指定的模块,默认名称服务器apnews.com 使用所有指定的模块,Cloudflare 的 1.1.1.1 解析器运行命令:
zdns MULTIPLE --multi-config-file="./multiple.ini" --input-file="input.csv"
ZDNS 可以针对递归解析器(例如,组织 DNS 服务器)运行 [默认行为],也可以内部自行执行递归。如果执行少量查找(例如,数百万次)且使用的 go 例程少于 10,000,通常最快的方法是使用常见的递归解析器,如 Cloudflare 或 Google。Cloudflare 几乎总是比 Google 快。这在查找流行名称时尤其明显,因为它们已被缓存,可以在一次往返中回答。当使用数万个并发线程时,考虑内部执行迭代,以避免对递归解析器造成拒绝服务或速率限制。
要执行本地递归,请使用 --iterative 标志运行 zdns。使用此标志时,ZDNS 将在已发布的根服务器(例如,198.41.0.4)之间循环选择。在迭代模式下,可以通过指定 --cache-size 控制本地缓存的大小,通过设置 --iteration-timeout 控制每次迭代的超时时间。--timeout 标志控制给定输入的整体解析超时时间(即所有迭代步骤的总和)。
ZDNS 的性能源于使用轻量级 Go 例程进行大规模并行化。这种架构有几个注意事项:
每个 Go 例程使用自己的专用网络套接字。因此,您需要能够打开与指定的线程数(通过 --threads)一样多的套接字(包括最大文件描述符和临时端口)。默认情况下,ZDNS 使用 1,000 个线程,这小于 Linux 默认最大 1024 个打开 FD。然而,它大于 Mac OS 默认的 256 个。您可以通过运行 ulimit -n 查看允许的最大打开 FD(以及套接字)数量。如果希望使用比该数量更多的线程,则需要增加操作系统级别的打开文件数。如果未能做到这一点,您将遇到类似 FATA[0000] unable to create socketlisten udp <client IP address>:0: socket: too many open files 的致命错误。如果希望运行比可用临时端口更多的线程,则需要使用多个客户端 IP 地址:--local-addr=A,B,C。
默认情况下,ZDNS 通过为每个轻量级例程在启动时创建一个未绑定的 UDP 套接字并将其用于所有查询(无论目标 IP 如何)来“重用”UDP 套接字。这极大地提高了性能,因为 ZDNS 和主机操作系统不需要为发送每个数据包而建立和拆除套接字(因为 DNS 查询/响应通常每个数据包一个)。然而,这意味着 ZDNS 将在启动时为每个线程预分配一个套接字。如果只查找少量名称,这可能不是最优的。例如,如果只需要查找 100 个名称,但使用默认的 1,000 个线程,您将绑定但从未使用 900 个 UDP 套接字。我们建议您为用例指定合理的线程数(这也避免了启动这些线程的任何工作),而不是担心回收套接字。这就是为什么即使只查找单个名称,您也可能收到无法打开大量套接字的错误。如果必须为每个查询创建新套接字,可以通过指定 --recycle-sockets=false 禁用此重用。
Go 会愉快地使用所有可用的 CPU 核心,并且如果指定了大量线程,可能会使用大量的 CPU。CPU 主要用于解析和 JSON 编码。如果要限制 CPU 核心数,可以通过包含 --go-processes=n 标志或设置 GOMAXPROCS 环境变量来实现。
很难推荐一个精确的 --threads 值,因为它取决于几个因素。下图显示了随着线程数增加,样本工作流运行时间降低但名称解析失败率升高的情况。
您看到的性能很大程度上取决于工作流、硬件以及名称服务器负载分布情况。如果您希望针对自己的工作流/硬件最大化性能,我们建议从 100 个线程开始,然后增加直到开始看到名称解析失败率升高。为帮助此过程,您可以使用 --output-file=output.jsonl 和 grep -v "NOERROR" output.jsonl | wc -l 来计数解析失败的名称数量。可能有助于调优性能的标志包括:
--timeout ZDNS 在单个名称上的最大花费时间--iteration-timeout ZDNS 在单个迭代步骤上的最大花费时间(例如:在 .com 层解析 google.com)--network-timeout ZDNS 等待名称服务器响应的最大时间--retries=N 如果在 --iterative 模式下连接特定名称服务器失败,ZDNS 将在该层使用另一个未查询的名称服务器重试。重试是按名称进行的,因此如果 --retries=1,ZDNS 将在其完整迭代过程中对一个名称针对新名称服务器重试一次。如果所有名称服务器都已查询,则将随机选择一个名称服务器。--name-servers 用于查找的名称服务器列表,主要在 --iterative=false 时有用DNS 包含许多并不总是有用的额外数据。有四种结果详细程度:short、normal(默认)、long 和 trace:
short:最简洁的结果输出。仅包含关于响应的信息normal:提供 short 中的所有内容以及关于响应服务器的数据long:输出服务器在 DNS 数据包中包含的所有内容,包括标志trace:输出递归过程的每一步中的所有内容用户还可以使用 --include-fields 标志并指定字段列表来包含特定的附加字段,例如 --include-fields=flags,resolver。附加字段有:class、protocol、ttl、resolver、flags、dnssec。
默认情况下,ZDNS 期望接收要在少量名称服务器上查找的名称列表。例如:
echo "google.com" | zdns A --name-servers=8.8.8.8,8.8.4.4
然而,有时您希望在大量服务器上查找相同的名称。这可以通过使用 名称服务器模式 来实现。例如:
echo "8.8.8.8" | zdns A --name-server-mode --override-name="google.com"
在这里,每一行通过管道传入 ZDNS 都会向 google.com 发送一个 A 查询。ZDNS 还支持通过传入逗号分隔的 name,nameServer 列表来混合和匹配两种模式。例如:
echo "google.com,8.8.8.8" | zdns A 将向 8.8.8.8 发送针对 google.com 的 A 查询,而不管 --name-servers= 标志指定了哪些名称服务器。没有明确指定名称服务器的行将像往常一样使用操作系统或 --name-servers 标志指定的服务器。
有一个功能可以对所有名称服务器执行特定的 DNS 查询。例如,您可能想要从某个域的所有名称服务器获取 A 记录。要这样做,您可以执行:
echo "google.com" | zdns A --all-nameservers
ZDNS 支持在单次调用中使用多个查找模块。例如,假设您想对一组域执行 A、AAAA 和 MXLOOKUP 操作,并且希望使用迭代解析。您需要使用 MULTIPLE 模块,并提供一个配置文件,其中包含您要使用的模块和模块特定标志。
请参阅 zdns --help 和 zdns <MODULE_NAME> --help 了解可以在配置文件中使用的全局和模块特定选项。
例如:
cat 1000k_domains.txt | zdns MULTIPLE --multi-config-file="./multiple.ini"
其中 multiple.ini 是一个看起来像这样的文件:
; Specify Global Options here
[Application Options]
iterative=true
; List out modules and their respective module-specific options here. A module can only be listed once
[MXLOOKUP]
ipv4-lookup = true
; You can use default values and just list modules if you don't need to specify any options
[A]
[AAAA]
示例 multiple.ini 文件可在 src/cli/multiple.ini 中找到
默认情况下,ZDNS 将使用 1,000 个轻量级 Go 例程运行。如果不小心,这将淹没许多上游 DNS 提供商。我们建议用户在执行任何扫描之前与本地网络管理员协调。您可以使用 --threads 和 --go-processes 命令行参数控制并发连接数。可以使用 --name-servers 指定备用名称服务器。ZDNS 将在发出请求时轮换使用这些服务器。我们已经成功运行了数万个轻量级例程的 ZDNS。
如果 zdns 遇到不支持的记录类型,它将生成一个输出记录,其中 type 字段设置正确,并在 unparsed_rr 字段中表示底层数据结构。不要依赖此字段的存在或结构。随着我们扩展对更多记录类型的支持,此字段(及其存在)可能随时更改。如果您发现自己正在使用此字段,请考虑提交一个添加解析器支持的拉取请求。
在 benchmark/ 中有一个可用的基准测试,可以以可预测的方式运行 ZDNS 并打印出一些运行统计信息。这对于在更改 ZDNS 前后比较性能很有用。更多详细信息请参见 benchmark README。
如果您有兴趣为 ZDNS 做出贡献,请参阅 CONTRIBUTING。
ZDNS 版权 2020 密歇根大学董事会
根据 Apache 许可证 2.0 版(“许可证”)许可;除非遵守许可证,否则您不得使用此文件。您可以在以下位置获取许可证副本:http://www.apache.org/licenses/LICENSE-2.0
除非适用法律要求或书面同意,否则根据许可证分发的软件按“原样”分发,不附带任何明示或暗示的保证或条件。请参阅许可证了解管理权限和限制的特定语言。