Skip to main content

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 在线验证工具

推荐使用以下在线工具:

  1. Kitterman SPF Validator

    • URL: https://www.kitterman.com/spf/validate.html
    • 功能: 语法检查, DNS 查询计数, 解析详情
  2. MXToolbox SPF Record Check

    • URL: https://mxtoolbox.com/spf.aspx
    • 功能: 记录验证, 警告和错误检测
  3. DMARCIAN SPF Inspector

    • URL: https://dmarcian.com/spf-survey/
    • 功能: 深度分析, 优化建议

检查项目:

✓ 语法正确性
✓ 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}%)")