Zum Hauptinhalt springen

Appendix C. Further Testing Advice (Weitere Testempfehlungen)

Appendix C. Further Testing Advice (Weitere Testempfehlungen)

Dieser Anhang bietet detaillierte Empfehlungen zum Testen und Validieren von SPF-Implementierungen.

C.1 Testen von SPF-Einträgen

C.1.1 Grundlegende Validierung

DNS-Abfragetest:

# SPF-Eintrag abfragen
dig example.com TXT | grep "v=spf1"

# Oder nslookup verwenden
nslookup -type=TXT example.com

# Oder host verwenden
host -t TXT example.com

Validierungspunkte:

  • ✓ Eintrag beginnt mit v=spf1
  • ✓ Nur ein SPF-Eintrag
  • ✓ Syntax korrekt (keine Rechtschreibfehler)
  • ✓ Eintragsgröße < 512 Oktetts

C.1.2 Online-Validierungstools

Empfohlene Online-Tools:

  1. Kitterman SPF Validator

    • URL: https://www.kitterman.com/spf/validate.html
    • Funktionen: Syntaxprüfung, DNS-Abfragezählung, Parsing-Details
  2. MXToolbox SPF Record Check

    • URL: https://mxtoolbox.com/spf.aspx
    • Funktionen: Eintragsvalidierung, Warnungen und Fehlererkennung
  3. DMARCIAN SPF Inspector

    • URL: https://dmarcian.com/spf-survey/
    • Funktionen: Tiefenanalyse, Optimierungsempfehlungen

Prüfpunkte:

✓ Syntax-Korrektheit
✓ DNS-Abfrageanzahl (≤ 10)
✓ Void lookup-Anzahl (≤ 2)
✓ Eintragslänge
✓ Mechanismus-Gültigkeit
✓ Include-Ketten-Tiefe

C.2 Testen der SPF-Prüfimplementierung

C.2.1 Unit-Test-Szenarien

Testfall 1: Grundlegende IP-Übereinstimmung

SPF-Eintrag: v=spf1 ip4:192.0.2.1 -all
Test-IP: 192.0.2.1
Erwartetes Ergebnis: pass

Test-IP: 192.0.2.2
Erwartetes Ergebnis: fail

Testfall 2: CIDR-Bereich

SPF-Eintrag: v=spf1 ip4:192.0.2.0/24 -all
Test-IP: 192.0.2.100
Erwartetes Ergebnis: pass

Test-IP: 192.0.3.1
Erwartetes Ergebnis: fail

Testfall 3: MX-Mechanismus

example.com MX-Eintrag: 10 mail.example.com
mail.example.com A-Eintrag: 192.0.2.10

SPF-Eintrag: v=spf1 mx -all
Test-IP: 192.0.2.10
Erwartetes Ergebnis: pass

Testfall 4: Include-Mechanismus

example.com: v=spf1 include:_spf.example.org -all
_spf.example.org: v=spf1 ip4:192.0.2.0/24 -all

Test-IP: 192.0.2.50
Erwartetes Ergebnis: pass

Testfall 5: Soft-Fail

SPF-Eintrag: v=spf1 ip4:192.0.2.1 ~all
Test-IP: 203.0.113.1
Erwartetes Ergebnis: softfail

C.2.2 Grenzfall-Tests

Leerer MAIL FROM:

MAIL FROM: <>
HELO: mail.example.com
Erwartet: HELO-Identität verwenden, local-part ist "postmaster"

Ungültiger Domainname:

MAIL FROM: [email protected]
Erwartetes Ergebnis: none (oder permerror)

DNS-Timeout:

DNS-Timeout simulieren
Erwartetes Ergebnis: temperror

Mehrere SPF-Einträge:

example.com TXT "v=spf1 mx -all"
example.com TXT "v=spf1 a -all"
Erwartetes Ergebnis: permerror

C.2.3 Limit-Tests

DNS-Abfragelimit-Test:

# Pseudocode
def test_dns_lookup_limit():
# SPF-Eintrag mit 11 includes erstellen
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-Eintrags-Limit-Test:

# Domain mit 11 MX-Einträgen erstellen
# Erwartet: mx-Mechanismus gibt permerror zurück

Void Lookup-Test:

# Include erstellen, das NXDOMAIN zurückgibt
# Zählung sollte in void lookup-Limit enthalten sein

C.3 Test-E-Mails senden

C.3.1 Testablauf

Schritt 1: Test-Domain vorbereiten

test.example.com IN TXT "v=spf1 ip4:YOUR_IP -all"

Schritt 2: Test-E-Mail senden

# swaks-Tool verwenden
swaks --to [email protected] \
--from [email protected] \
--server smtp.example.com

# Oder telnet verwenden
telnet smtp.recipient.com 25
HELO test.example.com
MAIL FROM:`&lt;[email protected]&gt;`
RCPT TO:`&lt;[email protected]&gt;`
DATA
Subject: SPF Test
.
QUIT

Schritt 3: E-Mail-Header prüfen

Received-SPF: pass (recipient.com: domain of [email protected]
designates YOUR_IP as permitted sender)
client-ip=YOUR_IP;
[email protected];

C.3.2 Tests für verschiedene Szenarien

Szenario 1: Pass-Test

Konfiguration: Von autorisierter IP senden
Erwarteter Header: Received-SPF: pass

Szenario 2: Fail-Test

Konfiguration: Von nicht autorisierter IP senden
Erwartet: E-Mail wird abgelehnt oder markiert
Erwarteter Header: Received-SPF: fail

Szenario 3: SoftFail-Test

Konfiguration: SPF-Eintrag verwendet ~all
Von nicht autorisierter IP senden
Erwartet: E-Mail wird akzeptiert aber markiert
Erwarteter Header: Received-SPF: softfail

Szenario 4: Neutral-Test

Konfiguration: SPF-Eintrag verwendet ?all
Erwarteter Header: Received-SPF: neutral

C.4 Makro-Tests

C.4.1 Makroerweiterungs-Validierung

Testfall:

Absender: [email protected]
Client-IP: 192.0.2.100

Makro %\{s} → [email protected]
Makro %\{l} → user
Makro %\{o} → example.com
Makro %\{d} → example.com
Makro %\{i} → 192.0.2.100
Makro %\{ir} → 100.2.0.192
Makro %\{d2} → example.com
Makro %\{d1} → com

Komplexer Makro-Test:

SPF: v=spf1 exists:%\{ir}.%\{l}._spf.%\{d} -all
Absender: [email protected]
IP: 192.0.2.100

Erweitert zu: 100.2.0.192.user._spf.example.com
Validierung: A-Eintrag für diesen Domainnamen abfragen

C.4.2 Makro-Trennzeichen-Test

Absender: [email protected]

%\{l} → user+tag
%\{l-} → user-tag (+ wird durch - ersetzt)
%\{lr} → gat+resu (umgekehrt)
%\{lr-} → gat-resu (umgekehrt und ersetzt)

C.5 Leistungstests

C.5.1 Antwortzeitests

Benchmark-Test:

# DNS-Abfragezeit testen
time dig example.com TXT

# Vollständige SPF-Prüfung testen
time spf_check example.com 192.0.2.1 [email protected]

Leistungsziele:

  • Einfacher Eintrag (1-2 Mechanismen): < 100ms
  • Komplexer Eintrag (mehrere includes): < 500ms
  • Maximal erlaubte Zeit: 20 Sekunden

C.5.2 Lasttests

Hohe Parallelität simulieren:

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 parallele SPF-Prüfungen testen
start = time.time()
results = check_spf_concurrent(1000)
end = time.time()

print(f"1000 Prüfungen abgeschlossen in: {end - start} Sekunden")
print(f"Durchschnitt pro Prüfung: {(end - start) / 1000 * 1000}ms")

C.6 Regressionstests

C.6.1 Test-Suite

Minimaler Testsatz:

1. Grundlegende pass/fail-Szenarien
2. Alle 7 Mechanismen (all, include, a, mx, ptr, ip4, ip6, exists)
3. Alle 4 Qualifizierer (+, -, ~, ?)
4. Beide Modifikatoren (redirect, exp)
5. Makroerweiterung
6. Fehlerbehandlung (temperror, permerror)
7. Limit-Tests (DNS-Abfragen, Timeout)

RFC 7208-Test-Suite:

# Offizielle Test-Suite verwenden
git clone https://github.com/openspf/openspf.git
cd openspf/tests
./run_tests.sh

C.6.2 Continuous Integration

CI/CD-Konfigurationsbeispiel:

# .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 Häufige Probleme testen

C.7.1 Veraltete E-Mail-Funktionen

Source Routing:

MAIL FROM:&lt;@relay.example.com:[email protected]>
Erwartet: example.com korrekt als Domain extrahieren

%-hack:

MAIL FROM:&lt;user%[email protected]>
Erwartet: Korrekt handhaben oder ablehnen

Bang Path:

MAIL FROM:&lt;[email protected]>
Erwartet: Korrekt handhaben oder ablehnen

C.7.2 Internationalisierte Domainnamen

IDN-Test:

Domain: münchen.de
A-label: xn--mnchen-3ya.de
SPF-Eintrag: Muss A-label verwenden

Test: Sicherstellen, dass korrekt konvertiert und abgefragt wird

C.7.3 IPv6-Tests

IPv6-Adressformate:

Vollformat: 2001:0db8:0000:0000:0000:0000:0000:0001
Komprimiertes Format: 2001:db8::1
IPv4-mapped: ::ffff:192.0.2.1

SPF: v=spf1 ip6:2001:db8::/32 -all
Test: Alle Formate sollten korrekt übereinstimmen

C.8 Debug-Techniken

C.8.1 Ausführliche Protokollierung aktivieren

Sender:

# 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

Empfänger:

# SPF-Debug-Protokollierung aktivieren
spf_debug_level = 5
tail -f /var/log/mail.log | grep SPF

C.8.2 dig-Verfolgung verwenden

SPF-Abfragen verfolgen:

# Vollständigen DNS-Auflösungsprozess anzeigen
dig +trace example.com TXT

# Include-Kette anzeigen
dig _spf.google.com TXT
dig _netblocks.google.com TXT

C.8.3 SPF-Debug-Tools verwenden

Python pyspf:

import spf

result, explanation = spf.check2(
i='192.0.2.1',
s='[email protected]',
h='mail.example.com'
)

print(f"Ergebnis: {result}")
print(f"Erklärung: {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 "Ergebnis: " . $result->code . "\n";

C.9 Produktionsumgebung-Überwachung

C.9.1 Überwachungsmetriken

Schlüsselmetriken:

- SPF pass-Rate
- SPF fail-Rate
- SPF softfail-Rate
- SPF temperror-Rate (sollte sehr niedrig sein)
- SPF permerror-Rate (sollte 0 sein)
- Durchschnittliche Prüfzeit
- DNS-Timeout-Rate

C.9.2 Alarmeinstellungen

Empfohlene Alarmregeln:

- permerror-Rate > 0%: Sofortiger Alarm (SPF-Eintragsfehler)
- temperror-Rate > 5%: Warnung (DNS-Probleme)
- fail-Rate plötzlicher Anstieg > 20%: Alarm (mögliche Konfigurationsänderung oder Angriff)
- Durchschnittliche Prüfzeit > 1 Sekunde: Warnung (Leistungsproblem)

C.9.3 Protokollanalyse

SPF-Fehler analysieren:

# IP-Adressen mit SPF fail extrahieren
grep "Received-SPF: fail" /var/log/mail.log | \
grep -oP 'client-ip=\K[0-9.]+' | \
sort | uniq -c | sort -rn

# Absenderdomains mit fail analysieren
grep "Received-SPF: fail" /var/log/mail.log | \
grep -oP 'envelope-from=\K[^;]+' | \
cut -d@ -f2 | sort | uniq -c | sort -rn

Bericht generieren:

# SPF-Statistikbericht-Generator
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}%)")