我前几天在整理自己手里几个域名的 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 长度=216

37 × 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 / 无=3

S2 和 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):

上面这些 DNS 数据都是 2026-10-21 当天用 DoH(阿里公共 DNS)实时查出来的,脚本在本机 Node v22.22.2 实跑,没有一条是我编的或者凭记忆写的。

最后修改:2026 年 10 月 21 日
如果觉得我的文章对你有用,请随意赞赏