Appendix C. Further Testing Advice (Conseils de test supplémentaires)
Appendix C. Further Testing Advice (Conseils de test supplémentaires)
Cette annexe fournit des recommandations détaillées pour tester et valider les implémentations SPF.
C.1 Test des enregistrements SPF
C.1.1 Validation de base
Test de requête DNS:
# Interroger l'enregistrement SPF
dig example.com TXT | grep "v=spf1"
# Ou utiliser nslookup
nslookup -type=TXT example.com
# Ou utiliser host
host -t TXT example.com
Points de validation:
- ✓ L'enregistrement commence par
v=spf1 - ✓ Un seul enregistrement SPF
- ✓ Syntaxe correcte (pas de fautes de frappe)
- ✓ Taille de l'enregistrement < 512 octets
C.1.2 Outils de validation en ligne
Outils en ligne recommandés:
-
Kitterman SPF Validator
- URL:
https://www.kitterman.com/spf/validate.html - Fonctionnalités: Vérification de syntaxe, comptage de requêtes DNS, détails d'analyse
- URL:
-
MXToolbox SPF Record Check
- URL:
https://mxtoolbox.com/spf.aspx - Fonctionnalités: Validation d'enregistrement, détection d'avertissements et d'erreurs
- URL:
-
DMARCIAN SPF Inspector
- URL:
https://dmarcian.com/spf-survey/ - Fonctionnalités: Analyse approfondie, recommandations d'optimisation
- URL:
Éléments à vérifier:
✓ Exactitude de la syntaxe
✓ Nombre de requêtes DNS (≤ 10)
✓ Nombre de void lookup (≤ 2)
✓ Longueur de l'enregistrement
✓ Validité des mécanismes
✓ Profondeur de la chaîne include
C.2 Test de l'implémentation de vérification SPF
C.2.1 Scénarios de tests unitaires
Cas de test 1: Correspondance IP de base
Enregistrement SPF: v=spf1 ip4:192.0.2.1 -all
IP de test: 192.0.2.1
Résultat attendu: pass
IP de test: 192.0.2.2
Résultat attendu: fail
Cas de test 2: Plage CIDR
Enregistrement SPF: v=spf1 ip4:192.0.2.0/24 -all
IP de test: 192.0.2.100
Résultat attendu: pass
IP de test: 192.0.3.1
Résultat attendu: fail
Cas de test 3: Mécanisme MX
example.com enregistrement MX: 10 mail.example.com
mail.example.com enregistrement A: 192.0.2.10
Enregistrement SPF: v=spf1 mx -all
IP de test: 192.0.2.10
Résultat attendu: pass
Cas de test 4: Mécanisme Include
example.com: v=spf1 include:_spf.example.org -all
_spf.example.org: v=spf1 ip4:192.0.2.0/24 -all
IP de test: 192.0.2.50
Résultat attendu: pass
Cas de test 5: Soft-fail
Enregistrement SPF: v=spf1 ip4:192.0.2.1 ~all
IP de test: 203.0.113.1
Résultat attendu: softfail
C.2.2 Tests de cas limites
MAIL FROM vide:
MAIL FROM: <>
HELO: mail.example.com
Attendu: Utiliser l'identité HELO, local-part est "postmaster"
Nom de domaine invalide:
MAIL FROM: [email protected]
Résultat attendu: none (ou permerror)
Timeout DNS:
Simuler un timeout DNS
Résultat attendu: temperror
Plusieurs enregistrements SPF:
example.com TXT "v=spf1 mx -all"
example.com TXT "v=spf1 a -all"
Résultat attendu: permerror
C.2.3 Tests de limites
Test de limite de requêtes DNS:
# Pseudo-code
def test_dns_lookup_limit():
# Créer un enregistrement SPF avec 11 includes
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"
Test de limite d'enregistrements MX:
# Créer un domaine avec 11 enregistrements MX
# Attendu: le mécanisme mx retourne permerror
Test de Void Lookup:
# Créer un include qui retourne NXDOMAIN
# Le comptage devrait être inclus dans la limite void lookup
C.3 Envoi d'e-mails de test
C.3.1 Processus de test
Étape 1: Préparer le domaine de test
test.example.com IN TXT "v=spf1 ip4:YOUR_IP -all"
Étape 2: Envoyer un e-mail de test
# Utiliser l'outil swaks
swaks --to [email protected] \
--from [email protected] \
--server smtp.example.com
# Ou utiliser telnet
telnet smtp.recipient.com 25
HELO test.example.com
MAIL FROM:`<[email protected]>`
RCPT TO:`<[email protected]>`
DATA
Subject: SPF Test
.
QUIT
Étape 3: Vérifier les en-têtes d'e-mail
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 de différents scénarios
Scénario 1: Test Pass
Configuration: Envoyer depuis une IP autorisée
En-tête attendu: Received-SPF: pass
Scénario 2: Test Fail
Configuration: Envoyer depuis une IP non autorisée
Attendu: L'e-mail est rejeté ou marqué
En-tête attendu: Received-SPF: fail
Scénario 3: Test SoftFail
Configuration: L'enregistrement SPF utilise ~all
Envoyer depuis une IP non autorisée
Attendu: L'e-mail est accepté mais marqué
En-tête attendu: Received-SPF: softfail
Scénario 4: Test Neutral
Configuration: L'enregistrement SPF utilise ?all
En-tête attendu: Received-SPF: neutral
C.4 Tests de macros
C.4.1 Validation d'expansion de macros
Cas de test:
Expéditeur: [email protected]
IP client: 192.0.2.100
Macro %\{s} → [email protected]
Macro %\{l} → user
Macro %\{o} → example.com
Macro %\{d} → example.com
Macro %\{i} → 192.0.2.100
Macro %\{ir} → 100.2.0.192
Macro %\{d2} → example.com
Macro %\{d1} → com
Test de macro complexe:
SPF: v=spf1 exists:%\{ir}.%\{l}._spf.%\{d} -all
Expéditeur: [email protected]
IP: 192.0.2.100
Étendu à: 100.2.0.192.user._spf.example.com
Validation: Interroger l'enregistrement A pour ce nom de domaine
C.4.2 Test de délimiteurs de macros
Expéditeur: [email protected]
%\{l} → user+tag
%\{l-} → user-tag (+ remplacé par -)
%\{lr} → gat+resu (inversé)
%\{lr-} → gat-resu (inversé et remplacé)
C.5 Tests de performance
C.5.1 Tests de temps de réponse
Test de référence:
# Tester le temps de requête DNS
time dig example.com TXT
# Tester la vérification SPF complète
time spf_check example.com 192.0.2.1 [email protected]
Objectifs de performance:
- Enregistrement simple (1-2 mécanismes): < 100ms
- Enregistrement complexe (plusieurs includes): < 500ms
- Temps maximum autorisé: 20 secondes
C.5.2 Tests de charge
Simuler une haute concurrence:
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
# Tester 1000 vérifications SPF concurrentes
start = time.time()
results = check_spf_concurrent(1000)
end = time.time()
print(f"1000 vérifications terminées en: {end - start} secondes")
print(f"Moyenne par vérification: {(end - start) / 1000 * 1000}ms")
C.6 Tests de régression
C.6.1 Suite de tests
Ensemble de tests minimal:
1. Scénarios pass/fail de base
2. Tous les 7 mécanismes (all, include, a, mx, ptr, ip4, ip6, exists)
3. Tous les 4 qualificateurs (+, -, ~, ?)
4. Les deux modificateurs (redirect, exp)
5. Expansion de macros
6. Gestion des erreurs (temperror, permerror)
7. Tests de limites (requêtes DNS, timeout)
Suite de tests RFC 7208:
# Utiliser la suite de tests officielle
git clone https://github.com/openspf/openspf.git
cd openspf/tests
./run_tests.sh
C.6.2 Intégration continue
Exemple de configuration 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 Test des problèmes courants
C.7.1 Fonctionnalités de messagerie anciennes
Source Routing:
MAIL FROM:<@relay.example.com:[email protected]>
Attendu: Extraire correctement example.com comme domaine
%-hack:
MAIL FROM:<user%[email protected]>
Attendu: Gérer correctement ou rejeter
Bang Path:
MAIL FROM:<[email protected]>
Attendu: Gérer correctement ou rejeter
C.7.2 Noms de domaine internationalisés
Test IDN:
Domaine: münchen.de
A-label: xn--mnchen-3ya.de
Enregistrement SPF: Doit utiliser A-label
Test: S'assurer de la conversion et de la requête correctes
C.7.3 Tests IPv6
Formats d'adresse IPv6:
Format complet: 2001:0db8:0000:0000:0000:0000:0000:0001
Format compressé: 2001:db8::1
IPv4-mapped: ::ffff:192.0.2.1
SPF: v=spf1 ip6:2001:db8::/32 -all
Test: Tous les formats devraient correspondre correctement
C.8 Techniques de débogage
C.8.1 Activer la journalisation détaillée
Expéditeur:
# 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
Destinataire:
# Activer la journalisation de débogage SPF
spf_debug_level = 5
tail -f /var/log/mail.log | grep SPF
C.8.2 Utiliser le traçage dig
Tracer les requêtes SPF:
# Voir le processus complet de résolution DNS
dig +trace example.com TXT
# Voir la chaîne include
dig _spf.google.com TXT
dig _netblocks.google.com TXT
C.8.3 Utiliser les outils de débogage SPF
Python pyspf:
import spf
result, explanation = spf.check2(
i='192.0.2.1',
s='[email protected]',
h='mail.example.com'
)
print(f"Résultat: {result}")
print(f"Explication: {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 "Résultat: " . $result->code . "\n";
C.9 Surveillance de l'environnement de production
C.9.1 Métriques de surveillance
Métriques clés:
- Taux de pass SPF
- Taux de fail SPF
- Taux de softfail SPF
- Taux de temperror SPF (devrait être très faible)
- Taux de permerror SPF (devrait être 0)
- Temps de vérification moyen
- Taux de timeout DNS
C.9.2 Configuration des alertes
Règles d'alerte recommandées:
- Taux de permerror > 0%: Alerte immédiate (erreur d'enregistrement SPF)
- Taux de temperror > 5%: Avertissement (problèmes DNS)
- Augmentation soudaine du taux de fail > 20%: Alerte (changement de configuration possible ou attaque)
- Temps de vérification moyen > 1 seconde: Avertissement (problème de performance)
C.9.3 Analyse des journaux
Analyser les échecs SPF:
# Extraire les adresses IP avec SPF fail
grep "Received-SPF: fail" /var/log/mail.log | \
grep -oP 'client-ip=\K[0-9.]+' | \
sort | uniq -c | sort -rn
# Analyser les domaines d'expéditeur avec fail
grep "Received-SPF: fail" /var/log/mail.log | \
grep -oP 'envelope-from=\K[^;]+' | \
cut -d@ -f2 | sort | uniq -c | sort -rn
Générer un rapport:
# Générateur de rapport de statistiques 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}%)")