我前几天在整理自己手里几个域名的 DNS 记录,顺手想确认一件事:邮件认证那三件套——SPF、DKIM、DMARC——我到底配齐没有。我一直以为配过了。
跑完第一遍查询结果有点难看:
=== 邮件认证体检:jikegang.com ===
【问题】
✗ 根域没有 SPF 记录:任何人都能用这个域名当信封发信
✗ 没有 MX 记录:这个域名收不了信(若确实只用子域收信可忽略)
✗ 没有 DMARC 记录:收件方不知道该拿伪造邮件怎么办三条全空。我这个域名一天发不了几封信,被冒用的实际危害有限,但它让我好奇另一件事:那些真的每天在发信的域名,配得到底怎么样。于是我把这套检查写成脚本,扫了一批。
先扫一批域名看看水位
DNS 查询我走的 DoH,本机 UDP 53 经常被运营商的递归服务器改答案,HTTPS 拿到的才是权威结果。核心就一个函数:
const https = require('https');
const DOH = 'https://dns.alidns.com/resolve';
function getTxt(name) {
return new Promise((resolve) => {
const req = https.get(`${DOH}?name=${encodeURIComponent(name)}&type=16`,
{ timeout: 15000 }, (res) => {
let buf = '';
res.on('data', (d) => (buf += d));
res.on('end', () => {
const j = JSON.parse(buf);
resolve({
status: j.Status,
values: (j.Answer || []).map((a) => JSON.parse(a.data)),
});
});
});
req.on('error', () => resolve({ status: -1, values: [] }));
});
}拿 24 个常见域名跑下来,结果比我预想的散:
总域名 24,有 MX 的 22
根域有 SPF(恰好1条) 17
根域无 SPF: github.com, microsoft.com, huawei.com, jikegang.com,
cloudflare.com, openai.com, wikipedia.org
SPF 收尾 qualifier 分布: {"~":2,"-":15}
DMARC 策略分布: {"reject":7,"none":5,"quarantine":9,"无":3}
无 DMARC 的 3 个:sina.com.cn, jikegang.com, wikipedia.org
DMARC p=none(只观察不处置)5 个:gmail.com, outlook.com, 163.com, 126.com, jd.com有一半结果我一开始读错了。github.com、microsoft.com 根域没 SPF,看着像重大疏漏,其实不是——它们根本不用根域发信,发信的是各种子域和专用域名。根域没 SPF 只说明"这个域名自己不发信",这反而是对的。
真正让我在意的在最后两行:24 个域名里,3 个连 DMARC 记录都没有,另有 5 个是 p=none。加起来 8/24 = 33.3% 的域名,对伪造邮件其实没有任何处置动作。
SPF 那个 10 次限制,只在"这封信是假的"时才用得完
SPF 有个几乎人人听过、但很少人算过的限制:一次评估最多只能做 10 次 DNS 查询。RFC 7208 §4.6.4 的原文是这么写的(核对日期 2026-10-21):
SPF implementations MUST limit the total number of those terms to 10 during SPF evaluation... If this limit is exceeded, the implementation MUST return "permerror".
计入的是 include、a、mx、ptr、exists 这几个机制和 redirect 修饰符,all/ip4/ip6/exp 不算。而且 §4.1 特别强调这是全局计数,不是每层递归各算 10 次。
我把 include/redirect 递归展开统计了一遍,meituan.com 直接爆表:
域名 命中第一个include 全部不匹配(走到 -all) 结论
--------------------------------------------------------------------------
meituan.com 1 次 11 次 ❌ PermError
tencent.com 1 次 10 次 ✅ ok(正好卡满)
qq.com 1 次 9 次 ✅ ok
zhihu.com 1 次 9 次 ✅ ok
163.com 1 次 7 次 ✅ ok
baidu.com 1 次 5 次 ✅ ok我第一版脚本只有右边那一列,看到 11 次就觉得"这家配崩了"。后来才意识到这是最坏情况:SPF 求值遇到第一个 match 就短路,合法邮件的 IP 通常在第一个 include 里就命中了,压根走不到后面。
于是我加了左边那列做对照,然后发现这个限制有个挺讽刺的性质:
它只在你最需要 SPF 说话的时候,才会被用满。 一封信是真的 → IP 在第一个 include 里 match → 花 1 次查询 → 通过。一封信是假的 → 谁都不 match → 必须把整棵树走完 → 11 次 → PermError。
也就是说,超限这件事几乎只在伪造邮件这条路径上发生。而 PermError 的后果是 SPF 结果变成一个永久错误,不再是干净的 fail。多数 DMARC 实现会把 permerror 当未通过处理,所以信还是挡得住;但规范边上那些"按本地策略兜底"的空间,就是这么被撑开的。
DKIM 是藏起来的,选择器只能猜
SPF 和 DMARC 都有固定的记录名——根域的 TXT 和 _dmarc 的 TXT,照着查就行。DKIM 不一样,它的记录在 <选择器>._domainkey.<域名>,而 DNS 里没有任何标准方法能"列出这个域有哪些选择器"。
所以验证 DKIM 只能猜。我列了 37 个常见选择器去撞:
选择器候选数: 37
域名 命中数 命中的选择器 NXDOMAIN数
------------------------------------------------------------------------------
google.com 0 — 37
qq.com 0 — 37
163.com 0 — 37
bilibili.com 2 selector1, selector2 35
└ selector1: k=rsa p 长度=216
xiaomi.com 1 selector1 36
└ selector1: k=rsa p 长度=21637 × 6 = 222 次查询,只撞出 3 条。google.com、qq.com、163.com 一个都没命中——不代表它们没配 DKIM,只代表它们的选择器不在我的候选名单里(selector1/selector2 是 Microsoft 365 那套的默认值,bilibili 和 xiaomi 顺着就撞上了)。
这件事的实际意义是:DKIM 配完之后没法从外面验证,除非你知道选择器名。 这也解释了为什么很多"我配好了 DKIM"最后是自欺——压根没人从外面验过一遍。
对齐才是真正的开关
三件套里最容易被跳过的是"对齐"这一步。SPF 通过不等于 DMARC 通过,RFC 7489 §3.1 要求的是:认证出来的那个域,得和 From: 头里的域对上。默认 adkim/aspf 都是 r(relaxed,比组织域),改 s 才要精确匹配。
我把实测到的 DMARC 策略代入几种场景跑了遍矩阵:
S1 裸伪造(什么都没有) 未对齐: none=5 / quarantine=9 / reject=7 / 无=3
S2 第三方代发(SPF 不对齐,DKIM 对齐) 对齐放行 24/24
S3 列表转发(SPF 挂,DKIM 仍对齐) 对齐放行 24/24
S4 列表转发且改了正文(两个都不对齐) 未对齐: none=5 / quarantine=9 / reject=7 / 无=3S2 和 S3 全放行,S1 和 S4 按策略处置。这个对比把三件套的分工说清楚了:
- SPF 挡的是"谁在发",一旦邮件被转发,信封域换成转发方,SPF 必挂——不是配置错了,是它天生扛不住转发。
- DKIM 挡的是"内容被动没动",只要签名覆盖的头部和正文没被改,转发多少次都还活着。
- 只有两个都不对齐,DMARC 才会真正动手。
所以"SPF 挂了"这件事本身不值得慌,值得慌的是 DKIM 也一起挂了。
我那个体检脚本第一版误报了
写完扫描我顺手做了个单域名体检工具,跑 gmail.com 的时候它报了一条:
✗ SPF 没有 all 收尾:未匹配时结果是 neutral,等于没表态我信了大概半分钟,然后去看原始记录:v=spf1 redirect=_spf.google.com。redirect 会把整条评估跳到目标记录去,本记录有没有 all 根本无所谓——~all 在 _spf.google.com 里。
这是典型的"跑得起来、结论不对"型 bug:脚本没崩、输出格式也对,就是判据漏了一个分支。修完之后它会跟着跳过去:
const all = p.mods.find((x) => x.mech === 'all');
const redirect = p.mods.find((x) => x.mech === 'redirect');
if (!all && redirect) {
// redirect 会把整条评估跳到目标记录,本记录有没有 all 都无所谓
out.ok.push('SPF 用 redirect 跳到 ' + redirect.value + '(all 收尾要看目标记录)');
} else if (!all) out.issues.push('SPF 没有 all 收尾:未匹配时结果是 neutral,等于没表态');顺带一提,同一个脚本扫 meituan.com 还报了另一条我本来不知道的:DMARC pct=80。它的策略是 quarantine,但只对 80% 的邮件生效——剩下 20% 按 RFC 7489 §6.6.4 的规定走本地分类。这是渐进收紧的正常做法,不是问题,只是从外面看不出来。
我的判断
扫完这一圈,我改了三个看法。
第一,p=none 是起点,不是终点。 Google 的发件人指南(support.google.com/a/answer/81126,核对日期 2026-10-21;本机直接访问超时,正文取自搜索缓存)写得很清楚:每天 5000 封以上必须配齐 SPF + DKIM + DMARC,而 DMARC 的最低要求就是 p=none。也就是说,业界合规线恰好落在"只收报告、不处置"这一档。达到这条线不等于安全,只等于不被判违规。24 个域名里 5 个停在 p=none,我猜大部分是配完就忘了,而不是有意观察。
第二,动手之前先数清楚有几个系统在替你发信。 邮箱服务商、营销平台、CRM、工单系统、财务系统自动发的对账单——每一个都要进 SPF 的 include,每一个都要单独配 DKIM。10 次查询预算听起来宽裕,但 tencent.com 已经用满 10 次、meituan.com 已经爆到 11 次,都是被一堆 include 吃掉的。真要加新服务,先把现有预算算一遍,别等 PermError 了才发现。
第三,三件套里 DKIM 最值钱。 它能扛转发、能从根域外独立验证,而且是唯一一个"内容被动没动"的证据。SPF 那条路径天生有转发的缺口,配得再干净也补不上。
至于我自己的域名,结论不复杂:根域不发信,那就明确声明不发——v=spf1 -all;再补一条 _dmarc 的 p=reject。域名一天发不了几封信,但"明确声明不发的域"和"什么都没说的域",在收件方眼里完全是两回事。
参考与核对(2026-10-21):
- RFC 7208(SPF):https://www.rfc-editor.org/rfc/rfc7208.txt —— §4.6.4 的 10 次查询上限与 permerror、§4.1 的全局计数
- RFC 7489(DMARC):https://www.rfc-editor.org/rfc/rfc7489.txt —— §3.1 对齐定义、relaxed/strict 默认值、§6.3 策略取值、§6.6.4 的 pct 采样
- RFC 6376(DKIM):https://www.rfc-editor.org/rfc/rfc6376.txt
- Google 发件人指南:support.google.com/a/answer/81126(本机访问超时,正文取自搜索缓存,未直接核到官方页面)
上面这些 DNS 数据都是 2026-10-21 当天用 DoH(阿里公共 DNS)实时查出来的,脚本在本机 Node v22.22.2 实跑,没有一条是我编的或者凭记忆写的。
