
本文详解如何准确、可靠地通过编程方式验证 spf、dkim 和 dmarc 三类 dns 记录,重点解决 dkim selector 动态性、dmarc 子域名规范、spf 多重包含解析等常见误判问题,并提供基于 dnspython 的生产级验证方案。
本文详解如何准确、可靠地通过编程方式验证 spf、dkim 和 dmarc 三类 dns 记录,重点解决 dkim selector 动态性、dmarc 子域名规范、spf 多重包含解析等常见误判问题,并提供基于 dnspython 的生产级验证方案。
在构建企业邮件安全监测系统或自动化合规检查工具时,仅依赖固定格式的 DNS 查询(如硬编码 dkim._domainkey.example.com)极易失效——正如 Ryanair 和 MarvelApp 的案例所示:其 DKIM 签名头中明确使用 s=15below 和未显式声明的自定义 selector,导致静态查询返回 NXDOMAIN,而实际邮件却通过了 Google 的 dkim=pass 验证。这揭示了一个核心事实:DKIM 不是“一个记录”,而是一组由签名头动态指定的公钥记录;SPF 和 DMARC 同样存在语法兼容性、策略阶段性和多服务叠加等复杂性。要实现与 Gmail “显示原始邮件”(Show Original)同等精度的认证结果还原,必须遵循协议规范,而非经验式猜测。
✅ 正确的程序化验证逻辑
1. SPF:查询根域名的 TXT 记录,但需深度解析语法
SPF 记录始终位于 example.com(即 @ 主机)的 TXT 类型 DNS 记录中,格式为 v=spf1 ...。关键点在于:
-
不可仅匹配字符串前缀:需完整解析机制(
ip4,include,a,mx等)并递归展开所有include(如include:_spf.google.com),验证其最终 IP 列表是否合法; -
拒绝无效语法:如多个
v=spf1声明、~all与-all混用、未闭合引号等,均会导致接收方视为失败; -
工具建议:使用 Kitterman SPF Validator 或
dnspython+pyspf库进行语义校验。
import dns.resolver
from pyspf import spf
def check_spf(domain: str) -> dict:
try:
answers = dns.resolver.resolve(domain, 'TXT')
spf_records = [r.to_text().strip('"') for r in answers
if r.to_text().startswith('"v=spf1') or 'v=spf1' in r.to_text()]
if not spf_records:
return {"valid": False, "reason": "No SPF TXT record found"}
# 使用 pyspf 进行语法与逻辑验证(支持 include 展开)
result = spf.check2(i='192.0.2.1', s=domain, h='example.com') # 模拟测试IP
return {"valid": result[0] == 'pass', "record": spf_records[0]}
except Exception as e:
return {"valid": False, "reason": str(e)}2. DKIM:必须从邮件头提取 selector,再查 DNS
DKIM 公钥记录路径为 <selector>._domainkey.<domain></domain></selector>,其中 <selector></selector> 完全由发件服务器在 DKIM-Signature 头的 s= 字段指定(如 Ryanair 的 s=15below → 查询 15below._domainkey.ryanair.com)。这是程序化验证最关键的跃迁点:
- ❌ 错误做法:固定查询
dkim._domainkey.example.com或default._domainkey.example.com; - ✅ 正确流程:
① 解析原始邮件头,定位DKIM-Signature:行;
② 正则提取s=([\w\-\.]+)值(注意支持连字符、点号等);
③ 构造 DNS 查询:{selector}._domainkey.{domain},类型为 TXT;
④ 验证返回记录是否含v=DKIM1且公钥(p=)非空。
import re
import dns.resolver
def extract_dkim_selector(dkim_header: str) -> str:
match = re.search(r's=([^;\s]+)', dkim_header)
return match.group(1).strip() if match else None
def check_dkim(domain: str, selector: str) -> dict:
try:
fqdn = f"{selector}._domainkey.{domain}"
answers = dns.resolver.resolve(fqdn, 'TXT')
for r in answers:
txt = r.to_text().strip('"')
if txt.startswith('v=DKIM1') and 'p=' in txt:
return {"valid": True, "record": txt, "fqdn": fqdn}
return {"valid": False, "reason": "No valid DKIM1 record with public key"}
except dns.resolver.NXDOMAIN:
return {"valid": False, "reason": f"DKIM record not found at {fqdn}"}
except Exception as e:
return {"valid": False, "reason": str(e)}
# 示例:从 Ryanair 邮件头提取
dkim_header = "DKIM-Signature: v=1; a=rsa-sha1; c=relaxed/relaxed; s=15below; d=care.ryanair.com; ..."
selector = extract_dkim_selector(dkim_header) # → "15below"
result = check_dkim("ryanair.com", selector) # 查询 15below._domainkey.ryanair.com3. DMARC:严格限定子域名 _dmarc
DMARC 策略记录必须且只能发布在 _dmarc.example.com 的 TXT 记录中(RFC 7489 §6.1)。常见误区包括:
- 查询
dmarc.example.com(错误,缺少下划线); - 在子域名(如
mail.example.com)发布 DMARC(无效,仅根域_dmarc生效); - 忽略
p=策略演进:p=none(监控)→p=quarantine(隔离)→p=reject(拒收),生产环境应避免长期停留none。
def check_dmarc(domain: str) -> dict:
try:
fqdn = f"_dmarc.{domain}"
answers = dns.resolver.resolve(fqdn, 'TXT')
for r in answers:
txt = r.to_text().strip('"')
if txt.startswith('v=DMARC1'):
# 解析关键策略
policy = re.search(r'p=([^;]+)', txt)
rua = re.search(r'rua=([^;]+)', txt)
return {
"valid": True,
"policy": policy.group(1) if policy else "none",
"report_uri": rua.group(1) if rua else None,
"record": txt,
"fqdn": fqdn
}
return {"valid": False, "reason": "No valid DMARC1 record"}
except dns.resolver.NXDOMAIN:
return {"valid": False, "reason": f"DMARC record not found at {fqdn}"}⚠️ 关键注意事项与最佳实践
- DNS 缓存与传播延迟:配置更新后,全球 DNS 生效可能需 24–48 小时,程序化检查应加入重试机制(如指数退避);
-
多值记录处理:SPF/DKIM/DMARC 均允许单域名存在多条 TXT 记录(RFC 4408 §3.1.1),需合并解析(
dns.resolver.resolve()默认返回全部); -
安全边界:
include机制可能引入第三方域风险,验证时应限制递归深度(建议 ≤3 层),防止 SSRF 或无限循环; -
生产就绪建议:
• 使用dnspython>=2.0(支持异步、EDNS、DNSSEC);
• 对高并发场景,集成aiodns实现异步批量查询;
• 结合真实邮件头验证(如解析 Gmail 的Authentication-Results头),交叉比对 DNS 结果与实际传递结果。
✅ 总结:构建可信邮件认证验证链
真正的健壮性不在于“查到记录”,而在于理解协议意图并模拟接收方行为:SPF 是 IP 白名单的逻辑展开,DKIM 是签名与公钥的密码学绑定,DMARC 是前两者的策略仲裁与反馈中枢。当你的程序能从任意一封通过 Gmail 认证的邮件中,精准还原出 s= 的 selector、正确解析嵌套 include、并识别 _dmarc 的策略强度时,它才真正具备了与专业邮件网关比肩的验证能力。将此逻辑封装为可复用模块,即可成为企业邮件安全基线扫描、SOC 自动化响应、或 SaaS 客户域名健康度看板的核心引擎。

















