Appendix C. Further Testing Advice (进一步的测试建议)
Appendix C. Further Testing Advice (进一步的测试建议)
本附录提供了关于测试和验证 SPF 实现的详细建议。
C.1 [测试 SPF 记录]
C.1.1 基本验证
DNS 查询测试:
# 查询 SPF 记录
dig example.com TXT | grep "v=spf1"
# 或使用 nslookup
nslookup -type=TXT example.com
# 或使用 host
host -t TXT example.com
验证点:
- ✓ 记录以
v=spf1开头 - ✓ 只有一个 SPF 记录
- ✓ 语法正确(无拼写错误)
- ✓ 记录大小 < 512 八位字节
C.1.2 在线验证工具
推荐使用以下在线工具:
-
Kitterman SPF Validator
- URL:
https://www.kitterman.com/spf/validate.html - 功能: 语法检查, DNS 查询计数, 解析详情
- URL:
-
MXToolbox SPF Record Check
- URL:
https://mxtoolbox.com/spf.aspx - 功能: 记录验证, 警告和错误检测
- URL:
-
DMARCIAN SPF Inspector
- URL:
https://dmarcian.com/spf-survey/ - 功能: 深度分析, 优化建议
- URL:
检查项目:
✓ 语法正确性
✓ DNS 查询数量(≤ 10)
✓ Void lookup 数量(≤ 2)
✓ 记录长度
✓ 机制有效性
✓ Include 链的深度
C.2 [测试 SPF 检查实现]
C.2.1 单元测试场景
测试用例 1: 基本 IP 匹配
SPF记录: v=spf1 ip4:192.0.2.1 -all
测试IP: 192.0.2.1
期望结果: pass
测试IP: 192.0.2.2
期望结果: fail
测试用例 2: CIDR 范围
SPF记录: v=spf1 ip4:192.0.2.0/24 -all
测试IP: 192.0.2.100
期望结果: pass
测试IP: 192.0.3.1
期望结果: fail
测试用例 3: MX 机制
example.com MX记录: 10 mail.example.com
mail.example.com A记录: 192.0.2.10
SPF记录: v=spf1 mx -all
测试IP: 192.0.2.10
期望结果: pass
测试用例 4: Include 机制
example.com: v=spf1 include:_spf.example.org -all
_spf.example.org: v=spf1 ip4:192.0.2.0/24 -all
测试IP: 192.0.2.50
期望结果: pass
测试用例 5: 软失败
SPF记录: v=spf1 ip4:192.0.2.1 ~all
测试IP: 203.0.113.1
期望结果: softfail
C.2.2 边界条件测试
空 MAIL FROM:
MAIL FROM: <>
HELO: mail.example.com
期望: 使用 HELO 标识, local-part 为 "postmaster"
无效域名:
MAIL FROM: [email protected]
期望结果: none (或 permerror)
DNS 超时:
模拟 DNS 超时
期望结果: temperror
多个 SPF 记录:
example.com TXT "v=spf1 mx -all"
example.com TXT "v=spf1 a -all"
期望结果: permerror
C.2.3 限制测试
DNS 查询限制测试:
# 伪代码
def test_dns_lookup_limit():
# 创建 SPF 记录, 包含 11 个 include
spf = "v=spf1"
for i in range(11):
spf += f" include:domain{i}.example.com"
spf += " -all"
result = check_spf(spf, "192.0.2.1", "[email protected]")
assert result == "permerror"
MX 记录限制测试:
# 创建有 11 个 MX 记录的域
# 期望: mx 机制返回 permerror
Void Lookup 测试:
# 创建返回 NXDOMAIN 的 include
# 计数应该包含在 void lookup 限制中
C.3 [发送测试邮件]
C.3.1 测试流程
步骤 1: 准备测试域
test.example.com IN TXT "v=spf1 ip4:YOUR_IP -all"
步骤 2: 发送测试邮件
# 使用 swaks 工具
swaks --to [email protected] \
--from [email protected] \
--server smtp.example.com
# 或使用 telnet
telnet smtp.recipient.com 25
HELO test.example.com
MAIL FROM:<[email protected]>
RCPT TO:<[email protected]>
DATA
Subject: SPF Test
.
QUIT
步骤 3: 检查邮件头部
Received-SPF: pass (recipient.com: domain of [email protected]
designates YOUR_IP as permitted sender)
client-ip=YOUR_IP;
[email protected];
C.3.2 不同场景测试
场景 1: Pass 测试
配置: 从授权 IP 发送
期望头部: Received-SPF: pass
场景 2: Fail 测试
配置: 从未授权 IP 发送
期望: 邮件被拒绝或标记
期望头部: Received-SPF: fail
场景 3: SoftFail 测试
配置: SPF 记录使用 ~all
从未授权 IP 发送
期望: 邮件被接受但标记
期望头部: Received-SPF: softfail
场景 4: Neutral 测试
配置: SPF 记录使用 ?all
期望头部: Received-SPF: neutral
C.4 [宏测试]
C.4.1 宏扩展验证
测试用例:
发件人: [email protected]
客户端IP: 192.0.2.100
宏 %{s} → [email protected]
宏 %{l} → user
宏 %{o} → example.com
宏 %{d} → example.com
宏 %{i} → 192.0.2.100
宏 %{ir} → 100.2.0.192
宏 %{d2} → example.com
宏 %{d1} → com
复杂宏测试:
SPF: v=spf1 exists:%{ir}.%{l}._spf.%{d} -all
发件人: [email protected]
IP: 192.0.2.100
展开为: 100.2.0.192.user._spf.example.com
验证: 查询此域名的 A 记录
C.4.2 宏分隔符测试
发件人: [email protected]
%{l} → user+tag
%{l-} → user-tag (+ 被替换为 -)
%{lr} → gat+resu (反转)
%{lr-} → gat-resu (反转并替换)
C.5 [性能测试]
C.5.1 响应时间测试
基准测试:
# 测试 DNS 查询时间
time dig example.com TXT
# 测试完整 SPF 检查
time spf_check example.com 192.0.2.1 [email protected]
性能目标:
- 简单记录(1-2个机制): < 100ms
- 复杂记录(多个 include): < 500ms
- 最大允许时间: 20 秒
C.5.2 负载测试
模拟高并发:
import concurrent.futures
import time
def check_spf_concurrent(num_checks):
with concurrent.futures.ThreadPoolExecutor(max_workers=100) as executor:
futures = [
executor.submit(check_spf, "example.com", "192.0.2.1", "[email protected]")
for _ in range(num_checks)
]
results = [f.result() for f in futures]
return results
# 测试 1000 次并发 SPF 检查
start = time.time()
results = check_spf_concurrent(1000)
end = time.time()
print(f"完成 1000 次检查耗时: {end - start}秒")
print(f"平均每次: {(end - start) / 1000 * 1000}ms")
C.6 [回归测试]
C.6.1 测试套件
最小测试集:
1. 基本 pass/fail 场景
2. 所有 7 种机制(all, include, a, mx, ptr, ip4, ip6, exists)
3. 所有 4 种限定符(+, -, ~, ?)
4. 两种修饰符(redirect, exp)
5. 宏扩展
6. 错误处理(temperror, permerror)
7. 限制测试(DNS 查询, 超时)
RFC 7208 测试套件:
# 使用官方测试套件
git clone https://github.com/openspf/openspf.git
cd openspf/tests
./run_tests.sh
C.6.2 持续集成
CI/CD 配置示例:
# .github/workflows/spf-tests.yml
name: SPF Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run SPF Tests
run: |
python -m pytest tests/test_spf.py
python tests/validate_spf_records.py
C.7 [常见问题测试]
C.7.1 古老的邮件功能
源路由 (Source Routing):
MAIL FROM:<@relay.example.com:[email protected]>
期望: 正确提取 example.com 作为域名
%-hack:
MAIL FROM:<user%[email protected]>
期望: 正确处理或拒绝
Bang Path:
MAIL FROM:<[email protected]>
期望: 正确处理或拒绝
C.7.2 国际化域名
IDN 测试:
域名: münchen.de
A-label: xn--mnchen-3ya.de
SPF记录: 必须使用 A-label
测试: 确保正确转换和查询
C.7.3 IPv6 测试
IPv6 地址格式:
完整格式: 2001:0db8:0000:0000:0000:0000:0000:0001
压缩格式: 2001:db8::1
IPv4映射: ::ffff:192.0.2.1
SPF: v=spf1 ip6:2001:db8::/32 -all
测试: 所有格式都应正确匹配
C.8 [调试技巧]
C.8.1 启用详细日志
发送方:
# Postfix
postconf -e "smtpd_sender_login_maps = hash:/etc/postfix/sender_login"
postconf -e "smtpd_sender_restrictions = reject_sender_login_mismatch"
tail -f /var/log/mail.log
接收方:
# 启用 SPF 调试日志
spf_debug_level = 5
tail -f /var/log/mail.log | grep SPF
C.8.2 使用 dig 追踪
追踪 SPF 查询:
# 查看完整的 DNS 解析过程
dig +trace example.com TXT
# 查看 include 链
dig _spf.google.com TXT
dig _netblocks.google.com TXT
C.8.3 使用 SPF 调试工具
Python pyspf:
import spf
result, explanation = spf.check2(
i='192.0.2.1',
s='[email protected]',
h='mail.example.com'
)
print(f"结果: {result}")
print(f"解释: {explanation}")
Perl Mail::SPF:
use Mail::SPF;
my $spf_server = Mail::SPF::Server->new();
my $request = Mail::SPF::Request->new(
versions => [1],
scope => 'mfrom',
identity => '[email protected]',
ip_address => '192.0.2.1',
helo_identity => 'mail.example.com'
);
my $result = $spf_server->process($request);
print "结果: " . $result->code . "\n";
C.9 [生产环境监控]
C.9.1 监控指标
关键指标:
- SPF pass 率
- SPF fail 率
- SPF softfail 率
- SPF temperror 率(应该很低)
- SPF permerror 率(应该为 0)
- 平均检查时间
- DNS 超时率
C.9.2 告警设置
建议告警规则:
- permerror 率 > 0%: 立即告警(SPF 记录错误)
- temperror 率 > 5%: 警告(DNS 问题)
- fail 率突然上升 > 20%: 告警(可能的配置变更或攻击)
- 平均检查时间 > 1秒: 警告(性能问题)
C.9.3 日志分析
分析 SPF 失败:
# 提取 SPF fail 的 IP 地址
grep "Received-SPF: fail" /var/log/mail.log | \
grep -oP 'client-ip=\K[0-9.]+' | \
sort | uniq -c | sort -rn
# 分析 fail 的发件人域名
grep "Received-SPF: fail" /var/log/mail.log | \
grep -oP 'envelope-from=\K[^;]+' | \
cut -d@ -f2 | sort | uniq -c | sort -rn
生成报告:
# SPF 统计报告生成器
def generate_spf_report(log_file):
results = {
'pass': 0, 'fail': 0, 'softfail': 0,
'neutral': 0, 'none': 0,
'temperror': 0, 'permerror': 0
}
with open(log_file) as f:
for line in f:
if 'Received-SPF:' in line:
for result in results:
if f'Received-SPF: {result}' in line:
results[result] += 1
total = sum(results.values())
for result, count in results.items():
percentage = (count / total * 100) if total > 0 else 0
print(f"{result}: {count} ({percentage:.2f}%)")