一个常见情形:你把 marketing.example.com 的 CNAME 从指向 Webflow 改成指向新的自托管落地页,DNS 控制台显示变更已生效。五分钟后同事说:“我电脑上还是旧设计。“你跑 dig +short marketing.example.com,拿回的是新 IP。同事的电脑问的是另一个解析器,那个解析器的缓存里可能还是旧记录。谁的缓存错了?
诚实的答案是”谁的都没错,只是变更还没传到每一个递归解析器而已”——但你没办法靠对一个解析器的一次 dig 来证明这一点。你需要快速问几个公共递归解析器并做对比。过去这意味着写 CLI 循环、买商用网络监控工具,或者去多地区查询网站。浏览器帮不上忙:网页打不开 UDP/53 套接字,因此说不了传统的 DNS。RFC 8484 在 2018 年标准化了 DNS-over-HTTPS,Cloudflare 和 Google 随即上线了允许跨域请求的公共 DoH 端点,这件事才有了转机。现在一次 fetch() 调用、一个 HTTPS 请求就能查询任意类型的记录,JSON 端点返回的是纯 JSON。
ZeroTool 的 DNS Lookup 就是对这些端点的封装。它就是你自己会写的那个 fetch(),外加按类型分组的记录卡片、dig 风格的原始视图,以及在你选 ALL 时对八种最常被问到的记录类型并发查询。下面是操作指南:DoH 实际是怎么跑的、每种记录类型在实战里告诉你什么,以及五个会咬到任何用错位置去排查 DNS 的人的坑。
一段话讲清 DNS-over-HTTPS
传统 DNS 查询是走 UDP/53(截断时回退到 TCP/53)的二进制包。浏览器对这两种协议都没有套接字 API。DoH 的解法是把同样的查询编码成 HTTPS 请求。有两种编码:RFC 8484 二进制(POST 携带 application/dns-message body,或者 base64url 编码塞进 GET ?dns=),以及更早由 Google 首创、Cloudflare 跟进的 JSON 风格(GET ?name=...&type=...,返回 JSON)。JSON 风格不在任何 RFC 里。Cloudflare 的文档说它沿用了 Google 的结构,所以两家的字段一致,细节差异见下文:
{
"Status": 0,
"TC": false,
"RD": true,
"RA": true,
"AD": false,
"CD": false,
"Question": [{ "name": "zerotool.dev", "type": 1 }],
"Answer": [
{ "name": "zerotool.dev", "type": 1, "TTL": 300, "data": "172.67.170.64" },
{ "name": "zerotool.dev", "type": 1, "TTL": 300, "data": "104.21.95.91" }
]
}
Status 是标准的 DNS RCODE(RFC 1035 §4.1.1,由 RFC 6895 扩展)。AD 是 DNSSEC 的 “Authenticated Data” 标志位,两家文档都写明:只有回答里的每条记录都通过验证时才为真。RFC 6840 §5.8 规定解析器只在查询带 DO 或 AD 位时才应置这个位;不带 do=1 时 Cloudflare 的 AD 时真时假(见下文)。TC 是截断标志;Cloudflare 的文档说,由于它支持最大响应大小,DoH 下这个值几乎总是 false。
工具发送 do=1(DNSSEC OK)和 cd=0(Checking Disabled = 0)。cd=0 让解析器做 DNSSEC 验证;do=1 让它带上签名。不带 do=1 时,Cloudflare 的 AD 不可靠:2026-09-30 每种查询各发 30 次,example.com 的 A、AAAA、NS 每次都是 "AD": false,MX 每次都是 true;更早一轮 20 次 A 查询里 true 和 false 各 10 次。带 do=1 的查询和 Google 的查询全部是 true。要靠 AD 判断,就要带 do=1。反过来,cd=1 让解析器跳过验证——对一个坏掉的区域做取证检查时有用,但日常查询不该这么做。
十种记录类型,以及它们告诉你什么
工具暴露十种类型。每一种都回答一个特定的运维问题。挑错类型,你会花上几个小时去追一个根本不存在的配置错误。
| 类型 | 编码 | 提问 | 缺失或错误时常见的运维故障 |
|---|---|---|---|
A | 1 | ”哪个 IPv4 地址在服务这个域名?“ | 站点在开发者机器上能打开、别人那里 404——旧 A 记录还没换成新 A。 |
AAAA | 28 | ”哪个 IPv6 地址在服务这个域名?“ | AAAA 仍指向旧主机时,优先用 IPv6 的客户端打不开网站,而只有 IPv4 的测试环境看不出问题。 |
CNAME | 5 | ”这个别名指向哪个规范名?“ | CNAME 指向一个已删除的 Heroku 应用,或者指向某 SaaS 的错误租户。 |
MX | 15 | ”哪些服务器接收这个域名的邮件?“ | 所有入站邮件被退回,提示 “no MX record found”,或迁移后投错了服务商。 |
TXT | 16 | ”发布了哪些策略字符串?“ | SPF 配成 soft-fail,DMARC 报告显示合法邮件被标垃圾,域名验证一直卡在 “pending”。 |
NS | 2 | ”谁对这个区域有权威?“ | 注册商转移后 NS 还指向旧服务商;更新完全不传播。 |
SOA | 6 | ”主服务器是谁,从服务器多久刷新一次?“ | 权威区域返回的是过期数据,因为从服务器缓存超出了 SOA 刷新窗口。 |
CAA | 257 | ”哪些 CA 可以为这个域名签发证书?“ | 没被列出的 CA 会拒绝签发,换 CA 或 CDN 后续签失败。 |
PTR | 12 | ”这个 IP 声称自己叫什么名字?“ | 邮件被退回,因为发送 IP 的 PTR 和 HELO 域名对不上;声誉服务把发件人标黑。 |
SRV | 33 | ”服务 _xmpp-client._tcp(等等)在哪里?“ | XMPP、SIP、LDAP 或 Matrix 联邦找不到主机,因为 SRV 缺失或端口写错。 |
工具的 ALL 模式用 Promise.all 对前八种类型并发请求,这是回答最常见运维问题”把这个区域的一切给我看看”的合理默认。PTR 和 SRV 不进 ALL,因为 PTR 要求你输入 in-addr.arpa 形式(比如 34.216.184.93.in-addr.arpa)而不是域名,SRV 要求带下划线前缀的特定名(_xmpp-client._tcp.example.com)。两者都更适合作为定向的单类型查询,而不是大扫荡的一部分。
为什么你的 dig 和工具结果对不上
最常见的意外:开发者笔记本上的 dig 和浏览器里的 DoH 对同一个域名给出不同答案。背后有四个无聊的原因和一个有意思的原因。
本地递归缓存。 你的笔记本、路由器或公司转发器会按每条记录的 TTL 缓存 DNS。CNAME 的 TTL 设为 86400(24h)意味着权威服务器上的变更最多要 24 小时才能传到你笔记本。DoH 调用问的是 Cloudflare 或 Google,他们的缓存与你的缓存互相独立。分别用 Cloudflare 和 Google 各查一次、对比答案——如果 Cloudflare 返回新值、Google 返回旧值,你正实时看着传播过程。
Split-horizon DNS。 企业网络、VPN 和 Active Directory 环境通常会跑一个内部权威解析器,对同一个域名返回不同答案。internal-tools.example.com 在防火墙内可能解析到 10.42.0.5,外网看就是 NXDOMAIN。DoH 调用看到的是外网视角。如果工具对一个你工作机能用的名字返回 NXDOMAIN,那这个区域就是 split-horizon,公共 DNS 根本不知道这个名字的存在。
地理路由的答案。 Cloudflare、Akamai、AWS Global Accelerator 以及大多数 CDN 会根据接收查询的边缘 POP 不同而返回不同的 A/AAAA。工具里看到的 IP 是 Cloudflare 或 Google 递归从他们的网络位置看到的,不是你的位置。对”这个 IP 对不对?“这种问题,工具给的是通用公网视角的答案;要问”从我的 POP 看这个 IP 对不对?“,就得从那个 POP 发起查询。
EDNS Client Subnet(RFC 7871)。 一些权威服务器在递归解析器转发客户端子网提示时,能返回更精准的地理路由答案。Google Public DNS 的文档说它”通常会发送大致的网络信息(一般是把 IPv4 地址的最后一段置零)“;Cloudflare 的 FAQ 说 1.1.1.1 不发送 ECS。因此同一个走地理路由的域名经两家查询可能拿到不同 IP,Google 的结果还取决于查询从哪里发出。Google 的 JSON 接口可以用 edns_client_subnet 参数自己指定子网(工具不发送该参数)。2026-09-30 用它查 www.qq.com:114.114.114.0/24 得到 121.14.77.201、121.14.77.221,8.8.8.0/24 得到 43.159.109.55,139.130.4.0/24 得到 43.168.224.173。给 Cloudflare 带同样的参数,它不理会,返回 43.159.109.55。
有意思的那个:陈旧的负缓存。 RFC 2308 规定 NXDOMAIN 响应可缓存。不存在的记录没有自己的 TTL,所以负缓存时长取 SOA 记录自身 TTL 与其 MINIMUM 字段中较小的那个(RFC 2308 §5)。如果某个区域被错配了一个小时、你的本地递归缓存了 NXDOMAIN,那么工具能返回正确答案的同时,你的本地查询会一直失败,直到这段时间过去。修法是等——或者在变更区域之前先把 SOA MINIMUM 调低。
读懂摘要 pill
记录视图上方有五个 pill,每个都压缩了一个运维信号:
- NOERROR / NXDOMAIN / SERVFAIL——RCODE。NOERROR 表示查询被正确处理;可能伴随的零条记录意思是”这个名字存在但没有该类型的记录”。NXDOMAIN 表示这个名字在该区域里根本不存在。SERVFAIL 表示递归解析器没法完成查询——通常是 DNSSEC 验证失败,或权威服务器返回了错误。
- N 条记录——Answer 段的记录数。当 NOERROR 配 0 条记录回来时,说明你挑的类型对这个名字是错的问题。去查这个名字对应区域的 NS。
- DNSSEC ✓ / no DNSSEC / DNSSEC failed——AD 标志位。出现 “no DNSSEC” 表示区域没有签名,或回答里的 CNAME 指向了没签名的区域;签名验证失败时返回的是 SERVFAIL。SERVFAIL 带有 DNSSEC 类扩展错误码(RFC 8914 的 1、2、5–12)时,标签显示 “DNSSEC failed”;其他 SERVFAIL 不显示这个标签,因为解析器没有给出可判断的回答。
com、org、dev这些 TLD 已签名(根区里都有它们的 DS 记录),但其下的域名要所有者自己开启 DNSSEC 才会签名:example.com已签名,zerotool.dev没有。 - Resolver——哪个 DoH 端点回的。在并排对比 Cloudflare 和 Google 时有用。
- Latency——从
fetch()发起到 JSON 解析完成的往返耗时。第一次查询包含与解析器建立 HTTPS 连接的时间,所以比之后的查询慢。
自己写同样的查询
工具存在的理由是:一次性查询点几下比写脚本快。要做自动化,DoH JSON 风格短到能塞进任何带 fetch 的语言。下面是三种参考实现,各自适合不同场景。
浏览器 / Node 18+ 版本拿到域名后只需两行:
const url = `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent('zerotool.dev')}&type=MX&do=1&cd=0`;
const res = await fetch(url, { headers: { accept: 'application/dns-json' } });
const json = await res.json();
console.log(json.Answer); // [{ name, type, TTL, data }, ...]
要超时就加 AbortSignal.timeout(5000),网络可能挂就包一层 try/catch。Cloudflare 和 Google DoH 返回的 JSON 结构跟上面一致,换提供商只需要换 URL。
Python 版用 httpx 或 urllib。仅用标准库的实现:
import json
import urllib.request
def doh(name: str, qtype: str, resolver: str = "cloudflare"):
if resolver == "google":
url = f"https://dns.google/resolve?name={name}&type={qtype}&cd=0"
headers = {}
else:
url = f"https://cloudflare-dns.com/dns-query?name={name}&type={qtype}&cd=0"
headers = {"accept": "application/dns-json"}
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req, timeout=5) as resp:
return json.loads(resp.read())
print(doh("zerotool.dev", "CAA"))
Shell 脚本和 CI 步骤里,curl 配 jq 是经典组合。下面这一行按优先级拉出一个域名的全部 MX 目标:
curl -s -H 'accept: application/dns-json' \
'https://cloudflare-dns.com/dns-query?name=cloudflare.com&type=MX' \
| jq -r '.Answer[] | "\(.data)"' | sort
外面套个 for 循环,你就有了自己的传播跟踪器,连终端都不用离开。把它包成 Bash 函数丢进 ~/.bashrc,你就用它替掉了 dig 一半的临时活儿。
TXT 分段——255 字节规则
长的 SPF、DKIM、DMARC 记录经常会突破 RFC 1035 §3.3.14 给 TXT 内单个字符串规定的 255 字节上限。DNS 协议允许一条 TXT 记录里有多个字符串;SPF 的约定(RFC 7208 §3.3)是把它们无分隔符地拼起来,作为逻辑记录。两家 JSON 接口的呈现方式不同:Cloudflare 给每段加引号、段间用空格分隔,和 dig 的输出一样;Google 把各段拼成一个值,不加引号。Cloudflare 的形式:
{
"name": "google.com",
"type": 16,
"TTL": 300,
"data": "\"v=spf1 include:_spf.google.com ~all\""
}
更长的记录会得到两段相邻字符串:
{
"data": "\"v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@example.com; \" \"fo=1; aspf=r; adkim=r\""
}
工具按解析器返回的原样显示。要从 Cloudflare 的形式得到逻辑值,去掉外层引号以及段间的”引号-空格-引号”(" ");SPF(RFC 7208 §3.3)和 DKIM(RFC 6376 §3.6.2.2)都规定各段直接相连、中间不加任何字符。Google 的形式已经拼好。以 cf2024-1._domainkey.zerotool.dev 的 DKIM 公钥为例,Cloudflare 返回 425 个字符,第一段 255 个字符之后有一处 " ";Google 返回的同一公钥是 420 个字符。
CAA——唯一能搞挂 TLS 签发的记录
CAA(RFC 8659)告诉公共证书颁发机构(CA),哪些 CA 被允许给某个域名签发证书。合规的 CA 签发前必须检查 CAA,没被任何 issue 记录列出的 CA 不得签发(RFC 8659 §3)。换到使用其他 CA 的 CDN 或 CA 时忘了更新 CAA,下次证书续签就会失败——这种限制从其他 DNS 查询里根本看不出来,只有 CAA 查询能告诉你它在那里。
一个允许 Let’s Encrypt 和 Google Trust Services(pki.goog)签发、禁止通配符证书的区域配置长这样:
example.com. 0 issue "letsencrypt.org"
example.com. 0 issue "pki.goog"
example.com. 0 issuewild ";"
example.com. 0 iodef "mailto:security@example.com"
issuewild ";" 禁掉所有通配符签发。iodef 告诉 CA:收到未授权的签发请求时把报告发到哪里。工具的 CAA 查询会按 tag(issue、issuewild、iodef)和值列出每条记录,方便你在触发签发前确认策略。
坑点
优先级 0、目标为 . 的 MX。 RFC 7505 定义了 “Null MX” 记录——0 .——意思是”这个域名彻底不收邮件”。如果工具对一个你以为能收邮件的域名返回 0 .,那是注册商或 DNS 服务商显式发布了这个占位记录。要改就得编辑区域文件,不是等传播。
根域(apex)上的 CNAME 是非法的。 有 CNAME 的名称不能再有其他记录(RFC 1034 §3.6.2、RFC 1912 §2.4),而区域根(example.com)必须有 SOA 和 NS。Cloudflare(CNAME 展平)和 Route 53(别名记录)提供看着像根上 CNAME 的记录,查询时按 A/AAAA 回答。对这类区域的根做 DoH 查询,拿到的是展平后的 A/AAAA,而不是 CNAME。这是正确行为,但会让期待看到控制台里那条 CNAME 的人困惑。
私有解析器与 Pi-hole。 运行 Pi-hole、NextDNS、AdGuard Home 或任何基于 DNS 的内容过滤的网络上,浏览器和系统的普通查询都会经过这个过滤器,可能被改写或屏蔽。工具的查询通过 HTTPS 发给公共解析器,过滤器看不到你查的域名;不过浏览器仍要经本地 DNS 解析 cloudflare-dns.com 或 dns.google 本身,过滤器可以屏蔽这两个名称。这也是为什么在你笔记本普通查询失败时工具还能给出真实答案。它有时有帮助(排查配错的 Pi-hole 规则),有时会误导(工具说域名正常,但你的网络屏蔽了它)。
EDNS Client Subnet 与 CDN 测试。 测 CDN 的地理路由时,DoH 返回的 IP 反映的是 Cloudflare 或 Google 的视角,不是你的。要测特定区域,用 DNS Checker 这类多地区查询工具,或者在那个区域的云上虚机里跑 dig。ZeroTool 的工具回答的是”这个公共解析器返回什么?“,不是”我在圣保罗的用户会看到什么?”。
DNSSEC 失败看起来像服务器故障。 如果某个区域 DNSSEC 链签错了,Cloudflare 和 Google 都会返回 SERVFAIL 加 AD=false,和解析器完全联系不上区域时的响应码相同。记录下方的解析器备注能区分两者:查 dnssec-failed.org 时,Cloudflare 的备注是 EDE(9): DNSKEY Missing ...(RFC 8914 扩展错误码 9)。Google 返回以 “DNSSEC validation failure” 开头的一句说明,外加 extended_dns_errors 字段(错误码 9),工具把后者显示为 EDE(9): No DNSKEY matches DS RRs of dnssec-failed.org。两家的标签都显示 “DNSSEC failed”。工具始终发送 cd=0,这是日常查询的正确默认;想看未经验证的答案用 dig +cd,要完整分析签名链用 DNSViz。
在 DNS 查询工具家族中的定位
下面每个工具回答的是不同的问题。
DNS Checker 从自己的服务器向”分布在全球多个地区的一组 DNS 服务器”查询记录,并画出传播地图。它回答的是”这次变更是不是每个地区都收到了?”。
Google Admin Toolbox Dig 把查询发给自己的后端(/apps/dig/lookup)。2026-09-29 测试时,它对 dnssec-failed.org 显示 “Record not found!”,只有原始视图里有 rcode SERVFAIL;对已签名的 example.com,标志行是 QR RD RA,没有 AD。
dig 和 kdig 是 CLI 参考实现,做脚本和需要指定权威服务器的查询(dig @ns1.example.com)时它们是对的答案。
ZeroTool 的工具填的是中间那一块:开发者想在浏览器里快速查一下,机器上装 dig 还得加个 Homebrew 包或者上 WSL,又想看公共解析器自己的回答,而不是经查询网站服务器转手的结果。你可以在 Cloudflare 和 Google 之间切换对比,看到 AD 标志和解析器备注,并复制原始 JSON。它不是做传播地图或纯权威查询的工具;那些场景,上面提到的工具更合适。
延伸阅读
站内:
- HTTP Header Analyzer——DNS 解析完成之后,服务器返回了什么。
- SSL Certificate Decoder——确认解析到的 IP 上服务的证书跟主机名匹配。
- URL Parser——在解析主机之前把 URL 拆成协议、主机、端口、路径、查询串。
- IP Subnet Calculator——拿到 A/AAAA 之后,推导防火墙规则用的子网结构。
站外:
- RFC 8484 — DNS Queries over HTTPS——DoH 标准。
- Cloudflare DoH 文档——端点、参数、JSON 风格。
- Google Public DNS DoH——Google 关于
dns.google/resolve的文档。 - RFC 1035 — Domain Names——原始 DNS 规范;记录格式参考首选。
- RFC 7208 — Sender Policy Framework——SPF,附 TXT 分段规则。
- RFC 8659 — CAA——Certification Authority Authorization。
- RFC 6891 — EDNS(0)——EDNS Client Subnet 依赖的扩展机制。