Categorie: Administration système
faq

Ce billet relate la mise en œuvre d'un pare-feu redondant en utilisant OpenBSD, Packet Filter, CARP et pfsync.

Même si cette mise en œuvre se situe dans un cadre scolaire (« bouh c'est nul, on ne fait pas comme ça en vrai ! » diront certains), je pense qu'elle vaut la peine d'être partagée. La partie installation d'OpenBSD est un peu bête mais quand c'est exigé ... L'installation d'un pare-feu redondant n'était qu'une partie d'un travail plus vaste, cela explique certaines choses (adressage IP, CPE, ...).

Ce travail a été réalisé avec Hamza Hmama et Frédéric Deveaux.

Il y a déjà une masse de tutoriels concernant une telle mise en œuvre sur le web. Celui-ci se démarquera en montrant, de manière assez précise, comment valider le bon fonctionnement du pare-feu.

Table des matières

Présentation détaillée

L'objectif est de déployer un pare-feu hautement disponible pour protéger des serveurs publics qui se situent dans une DMZ.

Le firewall est un service indispensable dans un réseau pour assurer la sécurité des connexions entrantes et sortantes. Utiliser un firewall redondant permet de diminuer énormément le risque que celui-ci soit hors service. En effet, il faut que toutes les machines physiques qui composent le cluster du firewall soient hors service (panne, maintenance, ...) à un instant précis, ce qui est moins probable qu'une seule machine.

Quelques exemples de déploiements d'une solution de ce type en entrée de gros sites : université de Rennes 1 (500 Mbps en entrée) et École normale supérieure. Voir : présentation « OpenBSD/Packet-Filter retour d'expérience » par Patrick LAMAIZIERE aux JRES 2013.

Pour ce faire, nous allons utiliser deux PC, sur lesquelles nous allons installer OpenBSD et configurer CARP et pfsync.

CARP est un protocole réseau qui autorise plusieurs machines à se partager un groupe d'IP dites virtuelles. Cela permet la haute disponibilité : pour que le service ne soit plus accessible, il faut que l'intégralité des machines qui se partagent le groupe d'IP soit hors service. CARP peut aussi servir à faire du partage de charge entre plusieurs machines (mode actif/actif). Nous n'utiliserons pas cette fonctionnalité mais resterons en mode actif/attente). CARP est l'équivalent du protocole propriétaire HSRP de Cisco et du protocole VRRP publié à l'IETF.

Pfsync permet de synchroniser les états de Packet Filter entre toutes les machines du cluster. Ainsi, une connexion qui a commencé en passant par FW1 n'est pas interrompue quand FW1 tombe (panne) et que FW2 prend la main.

Il n'existe aucun outil pour effectuer la synchronisation des règles du pare-feu entre les différentes machines du cluster. Un simple transfert sécurisé (scp) du jeu de règles puis un chargement des règles sur chaque machine avec ssh suffit amplement et s'automatise avec un simple script shell. Des scripts shell plus complets peuvent être trouvés sur le web. Exemple : Script to sync pf rules for CARP fws .

Voici un schéma de l'infrastructure que nous allons déployer :

Schéma de notre pare-feu hautement disponible

Schéma de notre pare-feu hautement disponible.

Nous utilisons un préfixe d'interconnexion /30 entre FW1 et FW2 pour le trafic pfsync. Le CPE est tout bêtement un PC qui exécute Dynamips. Derrière lui se trouve le réseau des utilisateurs.

Mise en œuvre

Il existe plusieurs manières de faire à chaque étape, plusieurs valeurs sont possibles pour chaque paramètre, ... Référez-vous à la documentation OpenBSD pour plus d'informations.

Installation d'OpenBSD

D'abord, on crée un médium d'installation. Malgré le fait d'avoir suivi plusieurs tutoriels disponibles sur le web (sauf ceux impliquant l'utilisation d'une machine virtuelle et/ou une première installation sur la clé elle-même, manque de temps), nous n'avons pas réussis à obtenir une clé USB bootable d'installation d'OpenBSD. Dans ce cas, il faut récupérer et graver l'ISO du CD d'installation de la dernière version stable d'OpenBSD. À l'heure actuelle, elle est disponible à cette URL : ftp://ftp.fr.openbsd.org/pub/OpenBSD/5.4/amd64/install54.iso.

Ensuite, il faut installer OpenBSD sur FW1 et FW2. On peut aussi effectuer une seule installation puis cloner le disque dur sur la deuxième machine, via le réseau, en utilisant Clonezilla. Ci-dessous, la procédure d'installation d'OpenBSD :

  1. Il faut aller dans le BIOS pour configurer le boot à partir du CD d'installation. Ce réglage dépend du type de BIOS, de la version, ... Dans notre cas, pour accéder à l'interface de configuration, il faut appuyer sur la touche « Suppr » du clavier au début de la procédure de démarrage du PC. Il faut ensuite se déplacer dans « Advanced BIOS Features » puis positionner la valeur « USB-CDROM » pour le paramètre « First Boot Device ». Il faut vérifier que le paramètre « Second Boot Device » est bien configuré à la valeur « Hard Disk ». Enfin, il faut revenir au menu principal à l'aide de la touche « Echap » et choisir « Save & Exit Setup ». Au reboot automatique, la machine démarrera sur le CD d'installation OpenBSD.
  2. À l'invite « boot> », il faut appuyer sur la touche « Entrée » du clavier.
  3. « (I)nstall, (U)pgrade or (S)hell? » : Choisir « Install » en tapant « I » et « Entrée ».
  4. « Choose your keyboard layout » : taper « fr » et « Entrée ».
  5. « System hostname? » : taper « FW1 » ou « FW2 » et « Entrée ».
  6. Configuration réseau :
    • « Which one do you wish to configure? » : « done » et « Entrée ».
    • « DNS domain name? » : accepter la proposition par défaut en appuyant sur « Entrée ».
    • « DNS nameservers? » : accepter la proposition par défaut en appuyant sur « Entrée ».
  7. « Password for root account? » : taper le mot de passe de votre choix (ici : « toor ») et « Entrée ». « again » : retaper votre mot de passe et « Entrée ».
  8. « Start sshd by default? » : accepter la proposition par défaut (yes) en appuyant sur « Entrée ».
  9. « Start ntpd by default? » : accepter la proposition par défaut (no) en appuyant sur « Entrée ». La synchronisation de l'horloge est importante, notamment pour la cohérence des logs mais comme notre maquette n'est pas connectée à Internet, nous ne pouvons accéder à aucun serveur de temps.
  10. « Do you expect to run the X Window System ? » : « no » et « Entrée ».
  11. « Setup a user? » : accepter la proposition par défaut (no) en appuyant sur « Entrée ».
  12. Configuration des disques durs/partitionnement :
    • « Which disk is the root disk? » : accepter la proposition par défaut en appuyant sur « Entrée ».
    • « Use DUIDs in fstab? » : accepter la proposition par défaut (yes) en appuyant sur « Entrée ».
    • « Whole disk or edit the MBR? » : accepter la proposition par défaut (whole) en appuyant sur « Entrée ».
    • « (A)uto layout, (E)dit auto layout, or create (C)ustome layout? » : accepter la proposition par défaut (auto layout) en appuyant sur « Entrée ».
  13. Source de l'installation :
    • « Location of sets? » : accepter la proposition par défaut (cd) en appuyant sur « Entrée ».
    • « Which one contains the install media? » : accepter la proposition par défaut (cd0) en appuyant sur « Entrée ».
    • « Pathname to the sets? » : accepter la proposition par défaut (5.4/amd64) en appuyant sur « Entrée ».
    • « Set name(s)? » : accepter la proposition par défaut (done) en appuyant sur « Entrée ».
  14. L'installation s'effectue ...
  15. « Location of sets » : nous ne voulons rien installer de plus donc accepter la proposition par défaut (done) en appuyant sur « Entrée ».
  16. « What timezone are you in? » : taper « Europe/Paris » puis « Entrée ».
  17. Taper « reboot » puis « Entrée ».

Configuration réseau

Configuration des interfaces réseau de FW1
echo "inet 170.16.3.129 255.255.255.248 NONE" > /etc/hostname.em0
echo "inet 170.16.3.193 255.255.255.192 NONE" > /etc/hostname.em1
echo "inet 192.168.1.1 255.255.255.252 NONE" > /etc/hostname.em2
Configuration des interfaces réseau de FW2
echo "inet 170.16.3.130 255.255.255.248 NONE" > /etc/hostname.em0
echo "inet 170.16.3.194 255.255.255.192 NONE" > /etc/hostname.em1
echo "inet 192.168.1.2 255.255.255.252 NONE" > /etc/hostname.em2
Configuration de la route par défaut sur FW1 et FW2

On configure une route par défaut via le CPE :

echo "170.16.3.132" > /etc/mygate
Activation de l'IP forwarding sur FW1 et FW2

On active l'ip forwarding en décommentant la ligne « net.inet.ip.forwarding=1 » dans le fichier /etc/sysctl.conf.

Configuration de l'interface réseau du CPE
conf t
  int fastEthernet 0/3
    ip address 170.16.3.132 255.255.255.248
    no sh
Ajout d'une route sur le CPE

Sur le CPE, on ajoute une route pour joindre le réseau des serveurs (170.16.3.192/26) via les FW

conf t
  ip route 170.16.3.192 255.255.255.192 170.16.3.131
Configuration de l'interface de notre serveur de test

Pour cela, on ajoute les lignes suivantes au fichier /etc/network/interfaces :

iface eth0 inet static
  address 170.16.3.196
  netmask 26
  gateway 170.16.3.195

CARP

Configuration des paramètres généraux de CARP sur FW1 et FW2

Pour cela, il faut rajouter les lignes suivantes dans le fichier /etc/sysctl.conf :

net.inet.carp.allow=1
net.inet.carp.preempt=1

« net.inet.carp.allow=1 » permet d'accepter les paquets du protocole CARP qui arrivent par le réseau.

« net.inet.carp.preempt=1 » permet d'optimiser l'élection du maître dans un groupe de redondance. Cela permet également de basculer toutes les interfaces d'un même groupe de redondance sur un même hôte en même temps.

Configuration des deux interfaces CARP sur FW1
echo "inet 170.16.3.131 255.255.255.248 170.16.3.135 vhid 1 carpdev em0 advskew 1 pass toor" > /etc/hostname.carp0
echo "inet 170.16.3.195 255.255.255.192 170.16.3.255 vhid 2 carpdev em1 advskew 1 pass toor" > /etc/hostname.carp1

« vidh X » : Virtual Host ID. C'est un numéro unique par réseau qui permet d'identifier un groupe de redondance CARP sur ce réseau. Les valeurs possibles sont comprises dans l'intervalle [1,255].

« carpdev » permet de spécifier l'interface réseau physique à utiliser pour ce groupe de redondance CARP.

« advskew X » permet d'introduire un biais pour forcer le choix lors de l'élection du maître du groupe de redondance. Plus la valeur de ce paramètre est élevée, moindre sont les chances de cet hôte de devenir le maître. Dans notre cas, c'est utile pour que les deux IP virtuelles (une sur le réseau d'interconnexion, l'autre sur le réseau des serveurs) soient attribuées au même hôte.

« pass <mot_de_passe> » est le mot de passe à utiliser lors de la communication avec les autres machines du même groupe de redondance. Évidemment, ce mot de passe doit être identifique sur toutes les machines d'un même groupe de redondance.

Configuration des interfaces CARP sur FW2
echo "inet 170.16.3.131 255.255.255.248 170.16.3.135 vhid 1 carpdev em0 advskew 100 pass toor" > /etc/hostname.carp0
echo "inet 170.16.3.195 255.255.255.192 170.16.3.255 vhid 2 carpdev em1 advskew 100 pass toor" > /etc/hostname.carp1

Configuration de Packet Filter

Règles de filtrage

On crée notre propre jeu de règles de filtrage minimaliste puis on l'insère dans le fichier /etc/pf.conf (en supprimant l'existant) de FW1 et FW2 afin qu'il soit appliqué automatiquement à chaque démarrage des deux machines :

# On bloque tout sauf ce qui passe sur la loopback
block
set skip on lo
 
# CARP
pass on { em0 em1 } proto carp keep state
 
# SSH depuis le réseau utilisateur vers les serveurs
pass in on em0 inet proto tcp from 170.16.3.0/24 to 170.16.3.196 port 22 keep state
 
# ICMP partout
pass inet proto icmp from any to any keep state

pfsync

Configuration de l'interface pfsync sur FW1
echo "up syncpeer 192.168.1.2 syncdev em2" > /etc/hostname.pfsync0

Par défaut, les mises à jour des états se font en multicast. Le paramètre « syncpeer » permet de faire les mises à jour d'état en unicast avec uniquement l'hôte spécifié.

« syncdev » permet de spécifier l'interface réseau physique à utiliser pour envoyer/recevoir le trafic pfsync.

Il faut noter que pfsync est assez gourmand en fonction de l'usage et donc du type de trafic qui passe par le pare-feu redondant, c'est pour cela qu'on conseille un lien réseau dédié à pfsync. Dans l'exemple de l'université de Rennes 1 cité plus haut, ils ont une majorité de trafic web. Rappel : pour une même page web, il faut établir XX connexions pour récupérer le contenu (js (comme jquery) stocké ailleurs, polices stockées ailleurs, pubs, ...). Cela génère des états au niveau des firewall et donc, du trafic pfsync pour échanger ces états. En pointe, ils observent 150 Mbps de trafic sur leur lien dédié pfsync.

Configuration de l'interface pfsync sur FW2
echo "up syncpeer 192.168.1.1 syncdev em2" > /etc/hostname.pfsync0
On autorise le protocole pfsync dans Packet Filter, sur FW1 et FW2

Pour cela, on ajoute la règle suivante aux fichiers /etc/pf.conf :

# pfsync
pass on em2 proto pfsync keep state

Valider le bon fonctionnement de notre pare-feu redondant

Il suffit de rebooter toutes les machines (FW1, FW2, serveur de test) pour que la configuration soit appliquée. Ou, de manière plus pragmatique, on peut appliquer les changements à chaud. Sur FW1 et FW2 :

# Recharger le réseau
sh /etc/netstart
 
# IP forwarding et CARP
sysctl -w net.inet.ip.forwarding=1
sysctl -w net.inet.carp.allow=1
sysctl -w net.inet.carp.preempt=1 
 
# Charger le jeu de règles dans PF et activer PF
pfctl -f /etc/pf.conf 
pfctl -e

Sur la machine de test :

ifup eth0

On peut ensuite vérifier que tout fonctionne comme attendu.

Connectivité

On vérifie que le réseau est fonctionnel en faisant passer un ping depuis une machine de test dans le réseau des utilisateurs vers notre serveur de test :

toto@client: # ping 170.16.3.196
PING 170.16.3.196 (170.16.3.196) 56(84) bytes of data.
64 bytes from 170.16.3.196: icmp_req=1 ttl=63 time=1.99 ms
64 bytes from 170.16.3.196: icmp_req=2 ttl=63 time=1.45 ms
64 bytes from 170.16.3.196: icmp_req=3 ttl=63 time=1.16 ms
64 bytes from 170.16.3.196: icmp_req=4 ttl=63 time=1.45 ms

CARP

On constate que CARP est fonctionnel en regardant la sortie de l'outil ifconfig.

Sur FW1 :

# ifconfig carp
carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
        lladdr 00:00:5e:00:01:01
        priority: 0
        carp: MASTER carpdev em0 vhid 1 advbase 1 advskew 1
        groups: carp
        status: master
        inet6 fe80::200:5eff:fe00:101%carp0 prefixlen 64 scopeid 0x6
        inet 170.16.3.131 netmask 0xfffffff8 broadcast 170.16.3.135
carp1: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
        lladdr 00:00:5e:00:01:02
        priority: 0
        carp: MASTER carpdev em1 vhid 2 advbase 1 advskew 1
        groups: carp
        status: master
        inet6 fe80::200:5eff:fe00:102%carp1 prefixlen 64 scopeid 0x7
        inet 170.16.3.195 netmask 0xffffffc0 broadcast 170.16.3.255

On constate que FW1 est bien le maître des deux groupes de redondances (« carp: MASTER »).

Sur FW2 :

# ifconfig carp
carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    lladdr 00:00:5e:00:01:01
    priority: 0
    carp: BACKUP carpdev em0 vhid 1 advbase 1 advskew 100
    groups: carp
    status: backup
    inet6 fe80::200:5eff:fe00:101%carp0 prefixlen 64 scopeid 0x6
    inet 170.16.3.131 netmask 0xfffffff8 broadcast 170.16.3.135
carp1: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    lladdr 00:00:5e:00:01:02
    priority: 0
    carp: BACKUP carpdev em1 vhid 2 advbase 1 advskew 100
    groups: carp
    status: backup
    inet6 fe80::200:5eff:fe00:102%carp1 prefixlen 64 scopeid 0x7
    inet 170.16.3.195 netmask 0xffffffc0 broadcast 170.16.3.255

On constate que FW2 est bien en backup sur les deux groupes de redondances (« carp: BACKUP »).

Une capture réseau depuis FW2 nous montre que FW1 est le maître sur l'IP virtuelle 170.16.3.131 et qu'en conséquence, il diffuse des messages « CARP advertisement » à fréquence régulière (ici : 1 seconde, paramétrable en changeant la valeur du paramètre « advbase ») :

# tcpdump -tttttvvni em0
tcpdump: listening on em0, link-type EN10MB
1.019374 carp 170.16.3.129 > 224.0.0.18: CARPv2-advertise 36: vhid=1 advbase=1 advskew=1 demote=0 (DF) [tos 0x10] (ttl 255, id 10529, len 56)
2.040085 carp 170.16.3.129 > 224.0.0.18: CARPv2-advertise 36: vhid=1 advbase=1 advskew=1 demote=0 (DF) [tos 0x10] (ttl 255, id 19703, len 56)
3.060098 carp 170.16.3.129 > 224.0.0.18: CARPv2-advertise 36: vhid=1 advbase=1 advskew=1 demote=0 (DF) [tos 0x10] (ttl 255, id 41787, len 56)
4.080206 carp 170.16.3.129 > 224.0.0.18: CARPv2-advertise 36: vhid=1 advbase=1 advskew=1 demote=0 (DF) [tos 0x10] (ttl 255, id 55471, len 56)

Si nous coupons l'interface carp0 sur FW1, celui-ci cesse d'émettre, les FW restants émettent des advertisement, une nouvelle élection a lieu entre les FW restants (dans notre cas : uniquement FW2) et FW2 devient le nouveau maître pour l'IP virtuelle 170.16.3.131 :

# tcpdump -tttttvvni em0
tcpdump: listening on em0, link-type EN10MB
5.099795 carp 170.16.3.129 > 224.0.0.18: CARPv2-advertise 36: vhid=1 advbase=1 advskew=1 demote=0 (DF) [tos 0x10] (ttl 255, id 16645, len 56)
6.119458 carp 170.16.3.129 > 224.0.0.18: CARPv2-advertise 36: vhid=1 advbase=1 advskew=1 demote=0 (DF) [tos 0x10] (ttl 255, id 20322, len 56)
7.4294686187 carp 170.16.3.129 > 224.0.0.18: CARPv2-advertise 36: vhid=1 advbase=255 advskew=255 demote=0 (DF) [tos 0x10] (ttl 255, id 33756, len 56)
7.4294688491 carp 170.16.3.130 > 224.0.0.18: CARPv2-advertise 36: vhid=1 advbase=1 advskew=100 demote=0 (DF) [tos 0x10] (ttl 255, id 22655, len 56)
8.129407 carp 170.16.3.130 > 224.0.0.18: CARPv2-advertise 36: vhid=1 advbase=1 advskew=100 demote=0 (DF) [tos 0x10] (ttl 255, id 31522, len 56)
10.4294507802 carp 170.16.3.130 > 224.0.0.18: CARPv2-advertise 36: vhid=1 advbase=1 advskew=100 demote=0 (DF) [tos 0x10] (ttl 255, id 35522, len 56)
11.4294916715 carp 170.16.3.130 > 224.0.0.18: CARPv2-advertise 36: vhid=1 advbase=1 advskew=100 demote=0 (DF) [tos 0x10] (ttl 255, id 35096, len 56)
12.359366 carp 170.16.3.130 > 224.0.0.18: CARPv2-advertise 36: vhid=1 advbase=1 advskew=100 demote=0 (DF) [tos 0x10] (ttl 255, id 63215, len 56)
14.4294736635 carp 170.16.3.130 > 224.0.0.18: CARPv2-advertise 36: vhid=1 advbase=1 advskew=100 demote=0 (DF) [tos 0x10] (ttl 255, id 17132, len 56)
15.179471 carp 170.16.3.130 > 224.0.0.18: CARPv2-advertise 36: vhid=1 advbase=1 advskew=100 demote=0 (DF) [tos 0x10] (ttl 255, id 32836, len 56)

Les logs (/var/log/messages) de FW2 nous montrent également la transition :

fw2 /bsd: carp0: state transition: BACKUP -> MASTER
fw2 /bsd: carp1: state transition: BACKUP -> MASTER

Quand FW1 est remis en état, il reprend la main et, dans les logs de FW2, nous lisons :

fw2 /bsd: carp1: state transition: MASTER -> BACKUP
fw2 /bsd: carp0: state transition: MASTER -> BACKUP

Pfsync

On vérifie que les interfaces pfsync sont up.

Sur FW1 :

# ifconfig pfsync0
pfsync0: flags=41<UP,RUNNING> mtu 1500
        priority: 0
        pfsync: syncdev: em2 syncpeer: 192.168.1.2 maxupd: 128 defer: off
        groups: carp pfsync

Note : « defer » est un paramètre qui peut être positionné à la valeur « on » pour faire en sorte qu'une connexion n'est réellement acceptée que si le pair pfsync a acquitté la modification de la table des états ou que le délai d'attente pour cet acquittement a expiré. Cela permet une plus grande cohérence entre les pare-feu et est indispensable lorsque plus d'un pare-feu doit s'occuper activement des paquets (typiquement : répartition de la charge).

Sur FW2 :

# ifconfig pfsync0
pfsync0: flags=41<UP,RUNNING> mtu 1500
        priority: 0
        pfsync: syncdev: em2 syncpeer: 192.168.1.1 maxupd: 128 defer: off
        groups: carp pfsync

On constate aussi l'existence d'un trafic réseau pfsync sur le lien d'interconnexion entre les deux pare-feux :

# tcpdump -tttttvvni em2
tcpdump: listening on em2, link-type EN10MB
0.007794 192.168.1.1 > 192.168.1.2: PFSYNCv6 len 276
    act UPD ST COMP count 3
    ...
 (DF) [tos 0x10] (ttl 255, id 38708, len 296)
0.410334 192.168.1.2 > 192.168.1.1: PFSYNCv6 len 276
    act UPD ST COMP count 3
    ...
 (DF) [tos 0x10] (ttl 255, id 22377, len 296)
0.410597 192.168.1.2 > 192.168.1.1: PFSYNCv6 len 108
    act UPD ST COMP count 1
    ...
 (DF) [tos 0x10] (ttl 255, id 51022, len 128)
0.410692 192.168.1.2 > 192.168.1.1: PFSYNCv6 len 108
    act UPD ST COMP count 1
    ...
 (DF) [tos 0x10] (ttl 255, id 40898, len 128)
0.410730 192.168.1.1 > 192.168.1.2: PFSYNCv6 len 276
    act UPD ST COMP count 3
    ...
 (DF) [tos 0x10] (ttl 255, id 65488, len 296)
0.410774 192.168.1.2 > 192.168.1.1: PFSYNCv6 len 108
    act UPD ST COMP count 1
    ...
 (DF) [tos 0x10] (ttl 255, id 24885, len 128)

Enfin, en établissant une session SSH depuis le réseau des utilisateurs vers notre serveur de test, alors que FW1 est le maître sur les deux interfaces CARP, nous constatons que les tables d'états des deux pare-feux sont synchronisées.

Sur FW1 :

# pfctl -ss
all carp 170.16.3.129 -> 224.0.0.18       SINGLE:NO_TRAFFIC
all carp 170.16.3.193 -> 224.0.0.18       SINGLE:NO_TRAFFIC
all pfsync 192.168.1.1 -> 192.168.1.2       MULTIPLE:MULTIPLE
all carp 224.0.0.18 <- 170.16.3.129       NO_TRAFFIC:SINGLE
all carp 224.0.0.18 <- 170.16.3.193       NO_TRAFFIC:SINGLE
all tcp 170.16.3.196:22 <- 170.16.3.132:33607       ESTABLISHED:ESTABLISHED
all tcp 170.16.3.132:33607 -> 170.16.3.196:22       ESTABLISHED:ESTABLISHED

Sur FW2 :

# pfctl -ss
all carp 170.16.3.129 -> 224.0.0.18       SINGLE:NO_TRAFFIC
all carp 170.16.3.193 -> 224.0.0.18       SINGLE:NO_TRAFFIC
all pfsync 192.168.1.2 <- 192.168.1.1       MULTIPLE:MULTIPLE
all carp 224.0.0.18 <- 170.16.3.129       NO_TRAFFIC:SINGLE
all carp 224.0.0.18 <- 170.16.3.193       NO_TRAFFIC:SINGLE
all tcp 170.16.3.196:22 <- 170.16.3.42:33607       ESTABLISHED:ESTABLISHED
all tcp 170.16.3.42:33607 -> 170.16.3.196:22       ESTABLISHED:ESTABLISHED

Si l'on coupe brutalement les interfaces réseau de FW1 (ou FW1 lui-même), on constate que FW2 prend le relais et que la session SSH n'est pas interrompue car les états des deux pare-feux sont cohérents et synchronisés.

Sources

RPKI+ROA et BIRD pour s’amuser

Aujourd'hui, nous allons voir comment utiliser RPKI+ROA avec BIRD et plus précisément comment prendre en compte les autorisations signées que sont les ROA comme éléments supplémentaires pour prendre une décision lors de la construction de la table de routage.

Table des matières

Si vous ne savez pas bien ce qu'est RPKI+ROA : sécuriser le routage sur Internet - RPKI+ROA.

On ne traitera pas de la manière de créer ses objets signés (ROA, Ghostbusters) quand on est opérateur ou LIR. Des pistes sont données dans le travail linké ci-dessus mais ça reste dans le cadre d'une maquette.

On se concentrera uniquement sur BIRD, une mise en pratique avec Quagga ayant déjà était réalisée dans le travail linké ci-dessus.

Pour sortir un peu du monde des maquettes, nous mettrons ça en pratique sur l'infra d'ARN, FAI associatif en Alsace. :)

« Pour s'amuser » dans le titre de ce billet signifie que ce que je vais vous présenter n'est pas la bonne manière de faire en production (exemples : programme additionnel, choix du cache-validateur, sécurité du dernier kilomètre, ...) et que ce billet rapporte juste une expérience, un délire, une envie de découvrir et de mettre en pratique, bref, un truc fait "for ze fun". Bref, ne suivez pas ce billet pour faire prendre des décisions de routing à des routeurs en production.

Bout d'essai

Nous allons créer une table destinée à stocker les VRP (les assertions « tel AS est autorisé à être à l'origine de tels préfixes » qui sont les contenus des ROA (VRP = Validated ROA Payload)) dans BIRD puis nous ferons prendre à BIRD une décision en fonction de la validité (ou non) d'une annonce BGP conformément à une assertion.

Dans bird6.conf (c'est pareil avec bird.conf, juste que c'est v4), on ajoute :

roa table testroa {
        roa 2001:660::/32 max 32 as 64501; # RENATER
}

On déclare donc une table des VRP nommée « testroa ». On la remplit avec une assertion : l'allocation v6 de RENATER doit être originée par l'AS 64501 (ASN faisant partie des ASN réservés à l'IANA pour faire des tests). On est d'accord que c'est faux (ASN de RENATER = 2200) mais, dans RPKI+ROA, les assertions font foi : pour BIRD, ça sera l'annonce BGP qu'il recevra qui sera incorrecte et non l'inverse.

Dans BIRD, l'opérateur « roa_check() » permet d'examiner la conformité d'une annonce BGP vis-à-vis d'une table de VRP (source : documentation BIRD - opérateurs). Il sort selon les 3 états normalisés (RFC 6811) :

  • ROA_UNKNOWN : aucun VRP ne couvre le préfixe annoncé.
  • ROA_VALID : au moins un VRP couvre le préfixe annoncé et l'AS d'un VRP correspond à l'AS "d'origine" indiqué dans l'annonce BGP.
  • ROA_INVALID : au moins un VRP couvre le préfixe annoncé mais aucun n'indique l'AS "d'origine" indiqué dans l'annonce BGP.

On peut donc utiliser roa_check() dans un filtre. Chez ARN, on a déjà un filtre en entrée de nos transitaires qui appelle plusieurs fonctions (une pour filtrer les martians, une pour forcer l'IP de sortie, ...). Voici le montage approximatif que l'on aura :

function verif_roa_test()
{
#       Nos manipulations avec les ROA.
}
 
filter cleaner 
{
        if avoid_martians() then
                accept;
 
        [...]
 
        if verif_roa_test() then
                accept;
 
        reject;
}
 
protocol bgp transitaire1 {
        debug all;
        description "transitaire1";
 
        local as 60630;
        neighbor 2001:db8::1 as 64502;
 
        import filter cleaner; 
        export filter arn_subnet;
}

Alors oui, on n'est pas obligé de faire une fonction mais on tient à laisser le filtre le plus clair possible.

Dans verif_roa_test(), nous pouvons mettre ce que nous voulons. Exemple :

if roa_check(testroa) = ROA_INVALID then return false;
 
return true;

Ici, si la vérification sort avec un état « invalide », l'annonce correspondante sera ignorée.

Pour vérifier, il suffit de demander à BIRD de relire la configuration et de créer la table des VRP (configure soft) puis de re-analyser chaque annonce (restart <nom_protocol>) :

# bird6c 
bird> configure soft
Reading configuration from /etc/bird6.conf
Reconfigured.
bird> restart transitaire1

On patiente un peu et on demande :

bird> show route 2001:660::/32
Network not in table
bird>

L'annonce concernant l'allocation v6 de RENATER a bien été ignorée car l'AS qui prétend en être à l'origine (2200) ne correspondait pas à celui indiquée dans l'assertion présente dans la table (AS 64501). Si vous n'obtenez pas ce comportement, alors vous recevez certainement une annonce pour cette alloc' via une autre session BGP (peering, autre transitaire).

Évidement, cet exemple est mauvais : un opérateur préférera toujours la connectivité à la sécurité car c'est son rôle : fournir de la connectivité. Donc, il ne faut pas rejeter les annonces dans le cas d'une vérification qui sort avec l'état « ROA_INVALID ». Je doute que ces annonces soient rejetées un jour sur des routeurs de production. Le fait de rejeter ces annonces ne permet aucune tolérance aux erreurs (erreur humaine dans la RPKI, erreur matérielle dans la RPKI, erreur humaine dans la création des ROA, ROA qui ne correspondent plus à la topologie, ...).

Le mieux est de leur attribuer une préférence locale différente permettant d'établir une hiérarchie : VALID > UNKNOWN > INVALID. Ainsi, si seulement une annonce invalide parvient au routeur, le préfixe de celle-ci sera quand même ajouté à la table de routage. Si des annonces valide et invalide cohabitent (ce qui est le cas lors d'une attaque par détournement de préfixe), alors l'annonce valide sera privilégiée.

Exemple de mise en pratique :

function verif_roa_test()
{
        if roa_check(testroa) = ROA_INVALID then bgp_local_pref = 90;
 
        if roa_check(testroa) = ROA_UNKNOWN then bgp_local_pref = 100;
 
        if roa_check(testroa) = ROA_VALID then bgp_local_pref = 110;
 
        return true;
}

Demandez à BIRD de recharger sa configuration et de re-analyser toutes les annonces et vous verrez que l'allocation v6 de RENATER se voit affecter une local-pref de 90 :

show route all 2001:660::/32
2001:660::/32      via 2001:db8::1 on eth0 [transitaire1 Oct03] * (100) [AS2200i]
	Type: BGP unicast univ
	BGP.origin: IGP
        [...]
	BGP.local_pref: 90 
        [...]

Soyez plus dynamique svp !

Bon, la vérification de la conformité des annonces vis-à-vis des VRP fonctionne mais si l'on doit ajouter toutes les assertions à la main ... Les routeurs doivent récupérer automatiquement ces assertions depuis un cache-validateur. Le protocole utilisé est RTR (RPKI To Router). Les routeurs doivent implémenter ce protocole. Ce n'est pas encore le cas de BIRD.

Néanmoins, BIRD permet de mettre à jour dynamiquement une table de VRP grâce à des commandes dans birdc(6) :

  • flush roa table <nom_table> : vider une table de toutes les assertions ajoutées dynamiquement (donc pas celles ajoutées depuis le fichier de configuration).
  • add roa <prefixe> max <int_max> as <ASN> table <nom_table> : ajouter une assertion dans une table.
  • del roa <prefixe> max <int_max> as <ASN> table <nom_table> : supprimer une assertion dans une table.

Il nous faut donc écrire un petit programme qui se connecte à un cache-validateur et, en fonction de ce qu'il reçoit, ajoute ou supprime des assertions dans notre table dans BIRD en utilisant birdc(6).

La RTRlib permet de se simplifier le travail puisqu'elle prend en charge la connexion à un cache-validateur et la réception des VRP. Je ne vous explique pas comment installer cette lib, c'est déjà très bien expliqué sur le wiki officiel : installation - RTRlib.

rtrclient est un petit logiciel fourni avec la RTRlib qui permet de la tester. Il se contente d'afficher les VRP qu'il récupère depuis un cache-validateur. Il suffit donc de modifier ce programme pour notre usage. En réalité, je me suis contenté :

  • D'ajouter un « flush roa table testroa » lors de l'initialisation pour éviter tout problème (programme qui plante, état incomplet, ...).
  • De commenter l'affichage « Prefix Prefix Length ASN ».
  • De modifier la fonction de callback « update_cb » pour changer l'action à exécuter pour chaque ajout ou suppression d'une assertion.

Ce n'est pas du grand art mais ça fait le job :

#include <stdlib.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include "rtrlib/rtrlib.h"
 
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
 
static void print_usage(char** argv){
    printf("Usage:\n");
    printf(" %s tcp <host> <port>\n", argv[0]);
 
    printf("\nExamples:\n");
    printf(" %s tcp rpki.realmv6.org 42420\n", argv[0]);
}
 
 
static void update_cb(struct pfx_table* p __attribute__((unused)), const pfx_record rec, const bool added){
    char ip[INET6_ADDRSTRLEN];
    ip_addr_to_str(&(rec.prefix), ip, sizeof(ip));
 
    char buff[100];
 
    if(added)
        sprintf(buff, "add roa %s/%u max %u as %u table testroa", ip, rec.min_len, rec.max_len, rec.asn);
    else
        sprintf(buff, "delete roa %s/%u max %u as %u table testroa", ip, rec.min_len, rec.max_len, rec.asn);
 
    if (fork() > 0)
    {
        /* On ne veut pas d'affichage informatif donc on redirige stdout vers /dev/null */
        int fd = open("/dev/null", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
        dup2(fd, 1);
        close(fd);
 
        if (rec.prefix.ver == IPV6)
	    execl("/usr/sbin/birdc6", "/usr/sbin/birdc6", buff, (char *) NULL);
	else
	    execl("/usr/sbin/birdc", "/usr/sbin/birdc", buff, (char *) NULL);
    }
}
 
 
 
int main(int argc, char** argv){
    enum mode_t { TCP, SSH } mode;
    char* host;
    char* port;
    char* user;
    char* privkey;
    char* pubkey;
    if(argc == 1){
        print_usage(argv);
        return(EXIT_FAILURE);
    }
 
    if(strncasecmp(argv[1], "tcp", strlen(argv[1])) == 0){
        if(argc != 4){
            print_usage(argv);
            return(EXIT_FAILURE);
        }
        mode = TCP;
        host = argv[2];
        port = argv[3];
 
    }
    else if(strncasecmp(argv[1], "ssh", strlen(argv[1])) == 0){
        if(argc != 7){
            print_usage(argv);
            return(EXIT_FAILURE);
        }
 
        mode = SSH;
        host = argv[2];
        port = argv[3];
        user = argv[4];
        privkey = argv[5];
        pubkey = argv[6];
    }
    else{
        print_usage(argv);
        return(EXIT_FAILURE);
    }
 
    tr_socket tr_sock;
    if(mode == TCP){
        tr_tcp_config config = {
            host,
            port,
        };
        tr_tcp_init(&config, &tr_sock);
    }
 
 
    rtr_socket rtr;
    rtr.tr_socket = &tr_sock;
 
    rtr_mgr_group groups[1];
    groups[0].sockets_len = 1;
    groups[0].sockets = malloc(1 * sizeof(rtr_socket*));
    groups[0].sockets[0] = &rtr;
    groups[0].preference = 1;
 
    rtr_mgr_config conf;
    conf.groups = groups;
    conf.len = 1;
 
 
    /* On vide la table pour éviter tout problème */
    if (fork() > 0)
        execl("/usr/sbin/birdc", "/usr/sbin/birdc", "flush roa table testroa", (char *) NULL);
    if (fork() > 0)
        execl("/usr/sbin/birdc6", "/usr/sbin/birdc6", "flush roa table testroa", (char *) NULL);
 
 
    rtr_mgr_init(&conf, 1, 520, &update_cb);
    rtr_mgr_start(&conf);
    //printf("%-40s %3s %3s %3s\n", "Prefix", "Prefix Length", "", "ASN");
    pause();
    rtr_mgr_stop(&conf);
    rtr_mgr_free(&conf);
    free(groups[0].sockets);
 
    return(EXIT_SUCCESS);
}

Je ne suis pas satisfait de l'absence de contrôle des valeurs de retour des appels systèmes, du traitement du buffer et des optimisations restantes. Mais je relativise : c'est juste un PoC et je m'en remets au formalisme et à la rigueur de la RTRlib.

Pour le compiler :

gcc rtrclient-bird.c -o rtrclient-bird -lrtr

Pour l'exécuter (exemple) :

./rtrclient-bird tcp rpki.realmv6.org 42420

Dans cet exemple, j'utilise le cache-validateur mis à disposition par les devs de la RTRlib. Je le fais sans sécurité ce qui est très mal dans un contexte de production surtout quand le cache-validateur n'est pas sur le même réseau L2 que le routeur ! Je signale que le cache-validateur rpki.realmv6.org peut aussi être utilisé over SSH. Voir : RTRlib - Usage.

Ce programme est un peu violent à son lancement : environ 6800 ROA à l'heure actuelle donc 6800 « birdc(6) roa add » donc 6800 fork() + exec() ... Mais en vitesse de croisière, avec +/- 1 ROA par-ci, par-là, ça se passe gentiment.

Pensez à supprimer/commenter le préfixe v6 de RENATER de votre table de VRP et à demander un « configure soft » à BIRD.

Voilà : la table des assertions dans BIRD se remplit toute seule et se maintient à jour toute seule (aussi longtemps que vit le programme rtrclient-bird). À partir de maintenant, BIRD prendra en compte ces assertions en fonction de votre filtre. Pour que ça soit rétroactif, il faut « restart <nom_protocol> ».

Faire des stats

Ce qu'on a vu plus haut, c'est du bonus, pour découvrir. Ce n'est clairement pas une bonne idée de faire tourner l'assemblage présenté ci-dessus (un logiciel maison mal-codé non-revu et qui ne sera pas suivi, une librairie installée en dehors d'un système de paquets, ...) sur une machine en production et surtout de baser une partie du processus de choix des routes sur cet assemblage.

Ce que je voulais obtenir, en vrai, c'est des statistiques comme celles de LACNIC ou celles du RIPE (exemple). Parce que c'est sympa d'avoir ses stats personnelles, que c'est formateur de chercher à les obtenir et aussi parce que l'outil de LACNIC est souvent en rade.

Et faire des stats, ça ne nécessite pas d'utiliser les VRP comme une donnée supplémentaire dans le processus de sélection des routes. Vous pouvez donc commenter la fonction verif_roa_test() ou supprimer son appel par le filtre (et « reconfigure » BIRD, of course).

Afficher le nombre d'assertions

birdc "show roa table testroa" | wc -l
birdc6 "show roa table testroa" | wc -l

Afficher le nombre de préfixes par état

birdc show route where roa_check(testroa) = ROA_VALID protocol transitaire1 count
birdc6 show route where roa_check(testroa) = ROA_VALID protocol transitaire1 count
 
birdc show route where roa_check(testroa) = ROA_INVALID protocol transitaire1 count
birdc6 show route where roa_check(testroa) = ROA_INVALID protocol transitaire1 count
 
birdc show route where roa_check(testroa) = ROA_UNKNOWN protocol transitaire1 count
birdc6 show route where roa_check(testroa) = ROA_UNKNOWN protocol transitaire1 count

Il n'est pas obligatoire de spécifier un protocole, c'est juste pour éviter les doublons si vous avez plusieurs transitaires, du peering, ...

Résultats

Voilà ce que l'on obtient, en v4, sur le routeur d'ARN :

birdc "show roa table testroa" | wc -l
5881
 
bird> show route protocol transitaire1 count 
463627 of 927572 routes for 463629 networks
 
bird> show route where roa_check(testroa) = ROA_VALID protocol transitaire1 count 
17167 of 927571 routes for 463628 networks
 
bird> show route where roa_check(testroa) = ROA_INVALID protocol transitaire1 count 
1609 of 927567 routes for 463626 networks
 
bird> show route where roa_check(testroa) = ROA_UNKNOWN protocol transitaire1 count 
444851 of 927571 routes for 463628 networks

Voilà, ce que l'on obtient, en v6, sur le routeur d'ARN :

birdc6 "show roa table testroa" | wc -l
949
 
bird> show route protocol transitaire1 count
14375 of 28809 routes for 14376 networks
 
bird> show route where roa_check(testroa) = ROA_VALID protocol transitaire1 count 
879 of 28809 routes for 14376 networks
 
bird> show route where roa_check(testroa) = ROA_INVALID protocol transitaire1 count 
59 of 28807 routes for 14375 networks
 
bird> show route where roa_check(testroa) = ROA_UNKNOWN protocol transitaire1 count 
13437 of 28809 routes for 14376 networks

On constate qu'environ 8% des routes v4 dont au moins un ROA leur est associé sont invalides. On est a 6% en v6.

On obtient des chiffres assez proches de ceux de LACNIC (attention à additionner v4 et v6 si vous voulez comparer) :

Route counts for the last 24 hours
 
Current INVALID route count for all repositories: 2000
 
Bad MaxLen: 1655
 
Wrong BGP Origin AS: 345
 
Current VALID route count for all repositories: 18277

On constate qu'environ 10% des routes dont au moins un ROA leur est associé sont invalides.

ÉDIT du 26/01/2014 à 20h45 : Voici les stats récoltées aujourd'hui sur le routeur d'ARN :
En v4 :

birdc-arn "show roa table testroa" | wc -l
7005
 
bird> show route protocol transitaire1 count 
474565 of 949513 routes for 474566 networks
 
bird> show route where roa_check(testroa) = ROA_VALID protocol transitaire1 count
18640 of 949510 routes for 474567 networks
 
bird> show route where roa_check(testroa) = ROA_INVALID protocol transitaire1 count
2102 of 949498 routes for 474559 networks
 
bird> show route where roa_check(testroa) = ROA_UNKNOWN protocol transitaire1 count
453815 of 949497 routes for 474558 networks

En v6 :

birdc6-arn "show roa table testroa" | wc -l
1094
 
bird> show route protocol transitaire1 count
15978 of 32033 routes for 15979 networks
 
bird> show route where roa_check(testroa) = ROA_VALID protocol transitaire1 count
999 of 32030 routes for 15978 networks
 
bird> show route where roa_check(testroa) = ROA_INVALID protocol transitaire1 count
60 of 32031 routes for 15978 networks
 
bird> show route where roa_check(testroa) = ROA_UNKNOWN protocol transitaire1 count
14917 of 32031 routes for 15978 networks

On constate qu'environ 10% (augmentation) des routes v4 dont au moins un ROA leur est associé sont invalides. On est a 6% en v6 (stagnation).

Le site web de LACNIC est down depuis plusieurs heures et depuis plusieurs points du réseau donc impossible de comparer.
Fin de l'édit

ÉDIT du 01/03/2014 à 14h30 : Voici les stats récoltées aujourd'hui sur le routeur d'ARN :
En v4 :

birdc "show roa table testroa" | wc -l
7654
 
bird> show route protocol transitaire1 count 
478065 of 956517 routes for 478066 networks
 
bird> show route where roa_check(testroa) = ROA_VALID protocol transitaire1 count 
19182 of 956497 routes for 478056 networks
 
bird> show route where roa_check(testroa) = ROA_INVALID protocol transitaire1 count 
2045 of 956463 routes for 478040 networks
 
bird> show route where roa_check(testroa) = ROA_UNKNOWN protocol transitaire1 count 
456811 of 956463 routes for 478039 networks

En v6 :

birdc6 "show roa table testroa" | wc -l
1202
 
bird> show route protocol transitaire1 count
16413 of 32905 routes for 16414 networks
 
bird> show route where roa_check(testroa) = ROA_VALID protocol transitaire1 count 
1078 of 32905 routes for 16414 networks
 
bird> show route where roa_check(testroa) = ROA_INVALID protocol transitaire1 count 
57 of 32905 routes for 16414 networks
 
bird> show route where roa_check(testroa) = ROA_UNKNOWN protocol transitaire1 count 
15276 of 32902 routes for 16413 networks

On constate qu'environ 10% (stagnation) des routes v4 dont au moins un ROA leur est associé sont invalides. On est a 5% en v6 (diminution). v4 et v6 cumulés : 9%

En comparaison, les chiffres obtenus par LACNIC :

Route counts for the last 24 hours
 
Current INVALID route count for all repositories: 2596
 
Bad MaxLen: 2065
 
Wrong BGP Origin AS: 531
 
Current VALID route count for all repositories: 20765
 
Dataset processed on: March 1, 2014

Fin de l'édit

Progression

En février 2012, le RIPE dénombrait environ 1800 ROA qui couvraient environ 8200 routes. Environ 40 % des routes dont au moins un ROA leur est associé étaient invalides.

En avril dernier, je dénombrais environ 3000 ROA.

Aujourd'hui, je dénombre environ 6800 ROA qui couvrent environ 19700 routes. Environ 8 % à 10 % des routes dont au moins un ROA leur est associé sont invalides.

ÉDIT du 26/01/2014 à 20h45 : Aujourd'hui, je dénombre environ 8100 ROA qui couvrent environ 21800 routes. Environ 8 % des routes dont au moins un ROA leur est associé sont invalides. Fin de l'édit

ÉDIT du 01/03/2014 à 14h30 : Aujourd'hui, je dénombre environ 8800 ROA qui couvrent environ 22300 routes. Environ 9 % des routes dont au moins un ROA leur est associé sont invalides. Fin de l'édit rss

contact

Sécuriser le routage sur Internet

Aujourd'hui, je vous propose un long billet sur le routage inter-domaine, sa sécurité actuelle et RPKI+ROA.

Attention : RPKI+ROA est encore un mécanisme tout jeune. Cela signifie que, bien que les concepts de base ne changeront pas et que ce travail date de mai 2013, certaines informations contenues dans ce travail vont devenir obsolètes à plus ou moins long terme.

Pour lire la version HTML, il suffit de cliquer sur le lien "Lire la suite" (et/ou de poursuivre ci-dessous). Pour ceux qui préfèrent lire un si gros pavé en PDF, c'est par là : Sécuriser le routage sur Internet.

Je mets également les sources LaTeX à votre disposition. Ces sources LaTeX peuvent servir de base à d'autres documents. Sources.

Vous pouvez également récupérer la maquette (vous comprendrez la raison de son existence en lisant le pavé). Elle peut servir pour mieux visualiser le fonctionnement de RPKI+ROA ou pour simplement tester ses différents composants. Elle repose sur LXC. Le tar contient la maquette compressée avec LZMA ainsi que les instructions d'utilisation au format texte. Maquette RPKI-ROA. Les fichiers de configurations principaux pour refaire une maquette from scratch sont disponibles ici : Fichiers de configuration principaux de ma maquette RPKI-ROA.

Et pour terminer, vous pouvez également récupérer le visuel projeté durant ma soutenance et ses sources. Support visuel soutenance | Sources LaTeX support visuel soutenance.

Si vous voulez tout récupérer (maquette, sources, pdf) en un seul coup, c'est par là : Sécuriser le routage sur Internet - Pack all-in-one.

Le tout (les sources LaTeX, les pdf, les images, la maquette, ...) est diffusé sous la licence habituelle de ce blog, à savoir : CC-BY-SA 3.0 France.

Lire la suite >>

Maquetter des réseaux avec LXC

Ce billet va tenter de résumer la manière dont j'utilise LXC pour créer et utiliser des maquettes de grande taille pour tester des choses (protocoles, logiciels, ...) ayant trait aux réseaux informatiques.

Table des matières

Pourquoi maquetter ?

Cela peut être utile de simuler la présence et les interactions d'un nouvel élément avant sa mise en production effective sur le réseau physique voire de maquetter un nouveau réseau complet pour voir s'il répond aux objectifs fixés avant d'acheter le matériel.

Maquetter permet aussi de découvrir et d'apprendre : mettre en œuvre des techniques, des protocoles, ... dans une maquette permet de les découvrir plus efficacement que d'étudier simplement la théorie.

Maquetter est aussi utile dans la recherche, pour faire des expériences et les rendre reproductibles, savoir quels facteurs sont aggravants (en les isolant les uns après les autres), trouver des solutions et les tester.

Les avantages de LXC pour maquetter

Bien que LXC puisse être utilisé pour isoler des applications, notamment pour un usage serveur, il présente également des avantages pour maquetter les machines d'un réseau.

LXC reste plus léger que de la virtualisation de type KVM : un ordinateur portable relativement modeste avec 3G de RAM fait tourner une maquette de 40 routeurs qui font de l'OSPF, du BGP et s'échangent un peu de trafic.

Dans un milieu plus "apprentissage/découverte", LXC est meilleur que Netkit (et ses frontend graphiques comme Marionnet), à mon avis, car LXC est maintenu activement et il offre la possibilité d'utiliser un noyau plus récent (2.26 avec Netkit, 2.19 avec Marionnet), ce qui n'est pas négligage quand on veut tester des fonctionnalités nouvelles ou quand certaines applications (notamment des démons) ont trop évoluées et ne sont plus capables de se binder sur un vieux noyau (il me semble avoir vu un cas avec Apache httpd). Sans compter la facilité de modification du système de base dans le cas de LXC (alors que c'est un véritable calvaire avec Netkit). Je ne sais pas ce qu'apporte (ou non) LXC comparé à User Mode Linux utilisé directement, sans passer par Netkit. ÉDIT du 19/09/2013 à 19h05 : Comme le mentionne Kartoch dans les commentaires, on peut utiliser Netkit sans avoir les droits root (modulo que /dev/shm ne doit pas être en noexec), ce qui a son intérêt dans une salle de TP à la fac/IUT/autre. Fin de l'édit

Néanmoins, autant dans un usage d'isolation des applications sur un serveur, je peux entrevoir les limites de LXC, la sécurité (un conteneur compromis qui permettrait de remonter au noyau et de mettre le zouk dans le reste du système/des autres conteneurs, cela ne me surprendrait pas), autant j'avoue ne pas savoir dans quels cas le fait que ce soit le même noyau dans l'hôte et dans tous les conteneurs est nuisible à un maquettage. Je peux simplement dire que cela me semble être utile dans les expériences aux timings serrés ou pour faire des mesures car la vision du temps est identique dans tous les conteneurs : l'instant T est le même dans tous les conteneurs.

Mettre des conteneurs LXC en réseau

Pour relier les conteneurs entre eux, on peut utiliser un ou plusieurs bridges. C'est intégré de base dans LXC et cela permet de voir tout le trafic de la maquette en dumpant simplement le trafic qui passe sur le bridge avec tcpdump/wireshark. Très pratique pour débugger, faire des stats, ... De plus, comme un bridge mémorise les MAC, comme un switch physique, le trafic unicast n'est pas transféré aux conteneurs qui n'en sont pas les destinataires.

Si l'on a besoin de fonctionnalités plus avancées (VLAN, agrégation de liens, ...), on peut utiliser Open vSwitch. J'avais testé pour découvrir : la communication entre conteneurs LXC juste marche. J'ai entendu dire que VDE ne fonctionnait pas avec les interfaces TAP de LXC. N'ayant jamais eu besoin de plus qu'un bridge, je n'ai jamais vérifié.

J'ignore s'il est possible d'interfacer LXC avec des routeurs Cisco/Juniper émulés. Dynamips semble pouvoir utiliser VDE mais comme LXC semble ne pas pouvoir ...

Adressage

Pour adresser/nommer votre maquette, je vous conseille fortement d'utiliser les ressources réservées à l'IANA : IPv4/v6, ASN, TLD, ... Cela fait sérieux et évite les collisions avec le réseau local (quand vous aurez eu à dépatouiller un tel cas, vous vous mettrez à utiliser les ressources réservées, je vous le garantit !).

Rendre la maquette plus réaliste

Au niveau réseau, Netem (network emulator) permet de rajouter de la latence, de la perte, de la corruption, ... sur les liens entre les différents conteneurs LXC. Pour que cela ne fasse pas trop artificiel, il est possible de faire varier la latence, la perte, ... en utilisant une distribution à la normale. On trouve plein d'exemples sur le web : Netem | The Linux Foundation, Simuler un lien WAN sous Linux chez NicoLargo, ... Netem est juste indispensable dès lors que l'on essaye de reproduire des réseaux réels ou, au moins, des conditions réelles.

Au niveau de chaque conteneur, les cgroups (control groups) permettent d'affecter des priorités d'accès aux ressources (CPU, réseau, ...) différentes pour chaque conteneur. Cela permet de favoriser un conteneur plutôt qu'un autre. Dans le cadre de maquettes réseau, cela permet de tenir compte des disparités que l'on trouve sur un réseau physique : on n'a des modèles de routeurs différents avec des capacités de traitement différentes ... Cela permet aussi de mesurer le comportement d'algorithmes de routage dans des situations défavorables. LXC lui-même est basé sur les cgroups. Je n'ai pas encore utilisé les cgroups dans une maquette donc je n'ai pas de ressources à conseiller plus que d'autres.

Automatiser le tout

Je vais vous montrer comment j'automatise la création et la gestion d'une grosse maquette avec des conteneurs LXC. L'ennui, c'est qu'il y a du besoin spécifique dedans (routing, forward, ...). Je vais essayer de tout bien vous expliquer pour que vous puissiez adapter à vos projets.

Preseed-file

Mes conteneurs sont tous basés sur Debian puisque Debian, c'est la vie. Pour créer des dizaines de conteneurs LXC à la chaîne, il est nécessaire d'avoir un preseed-file bien foutu qui configurera une bonne partie du conteneur (login, logiciels supplémentaires à installer, ...) lors de la création dudit conteneur sans poser aucune question dans une interface semi-graphique (ça ne passe pas à l'échelle ce genre de chose). Or, un preseed-file bien foutu, qui juste marche, bah ça ne court pas le web.

Voici celui que j'utilise, issu de quelques trouvailles personnelles minoritaires et de la fusion de ces deux modèles : Creating a Debian sliver template sur wiki.confine-project.eu et Debian wheezy preseed file for lxc-create -t debian lxc version 0.9.0.alpha2.

Pour le résumer :

  • Les conteneurs auront l'architecture amd64 et seront créés à partir du dépôt sid ;
  • Packages supplémentaires : less iputils-ping iptables telnet traceroute wget nano rsyslog bash-completion quagga et openssh-server ;
  • Utilisation d'un bridge nommé br0 ;
  • MAC fixe (on la remplacera plus loin vu que LXC ne sait pas faire tout seul) ;
  • On ne supprime pas la capabilitie « sys_admin » car elle est nécessaire à Zebra ;
  • On ne crée pas un compte utilisateur normal ;
  • Le mot de passe root est « toor » ;
  • On utilise le fuseau horaire « Europe/Paris » ;
# # Debian preseed file for CONFINE sliver template
# Tested on lxc 0.9.0~alpha3-2 and live-debconfig 4.0~a17.
 
# ## Distribution and packages
lxc-debconfig lxc-debconfig/distribution string sid
lxc-debconfig lxc-debconfig/architecture string amd64
lxc-debconfig lxc-debconfig/archives multiselect none
lxc-debconfig lxc-debconfig/mirror string ftp://ftp2.fr.debian.org/debian/
lxc-debconfig lxc-debconfig/archive-areas multiselect main
lxc-debconfig lxc-debconfig/packages string less iputils-ping iptables telnet traceroute wget nano rsyslog bash-completion quagga
 
# ## Network
# Please adjust to the name of the bridge used in your host.
lxc-debconfig lxc-debconfig/eth0-bridge string br0
# Private MAC address, to be replaced on sliver creation.
lxc-debconfig lxc-debconfig/eth0-mac string 52:C0:A1:AB:BA:1A
# Private veth interface name, to be replaced on sliver creation.
#lxc-debconfig lxc-debconfig/eth0-veth string veth-sliver
 
# ## Other container options
lxc-debconfig lxc-debconfig/auto boolean false
# Use live-debconfig to further configure the container.
lxc-debconfig lxc-debconfig/lxc-debconfig-with-live-debconfig boolean true
lxc-debconfig lxc-debconfig/apt-recommends boolean false
# Avoid debconf questions.
lxc-debconfig lxc-debconfig/debconf-frontend select noninteractive
## (default value)
##lxc-debconfig lxc-debconfig/debconf-priority string medium
 
# For running commands in the container and host at the end.
#lxc-debconfig lxc-debconfig/late-command string
#lxc-debconfig lxc-debconfig/late-host-command string 
 
 
# Capabilities to be dropped from the container.
lxc-debconfig lxc-debconfig/capabilities string \
    audit_control audit_write ipc_lock mac_admin mac_override \
    sys_module sys_pacct sys_rawio sys_resource sys_time \
    syslog wake_alarm
 
# For mounting filesystems in container.
#lxc-debconfig lxc-debconfig/mount0/entry string
##lxc-debconfig lxc-debconfig/mount0/comment string \
##    Bind mount host path in container
 
# ## Live-debconfig scripts configuration
 
# (For some reason live-debconfig options must be on a single line
# or the following options are not interpreted correctly.)
 
live-debconfig live-debconfig/components multiselect openssh-server, passwd
 
# ### LXC (sysvinit)
# Perform LXC tweaks in the container.
live-debconfig live-debconfig/sysvinit/lxc-enable boolean true
## (default values)
##live-debconfig live-debconfig/sysvinit/lxc-consoles string 6
##live-debconfig live-debconfig/sysvinit/lxc-disable-services string checkroot.sh hwclockfirst.sh hwclock.sh kmod module-init-tools mountall.sh mountkernfs.sh umountfs umountroot
 
### Hardware clock access (util-linux)
live-debconfig live-debconfig/util-linux/hwclockaccess boolean false
 
# ### Host name (hostname)
# Host name, to be replaced on sliver creation.
live-debconfig live-debconfig/hostname/hostname string sliver
 
# ### Network configuration (ifupdown)
live-debconfig live-debconfig/ifupdown/lo-comment string The loopback interface
live-debconfig live-debconfig/ifupdown/lo-enable boolean true
 
# Private interface method, to be replaced on sliver creation.
live-debconfig live-debconfig/ifupdown/eth0-ipv4-comment string The private interface
live-debconfig live-debconfig/ifupdown/eth0-ipv4-method select dhcp
 
# For static configuration of network interfaces.
##live-debconfig live-debconfig/ifupdown/eth0-ipv4-method select static
##live-debconfig live-debconfig/ifupdown/eth0-ipv4-address string 1.2.3.4
##live-debconfig live-debconfig/ifupdown/eth0-ipv4-netmask string 255.255.255.0
##live-debconfig live-debconfig/ifupdown/eth0-ipv4-gateway string 1.2.3.1
##live-debconfig live-debconfig/ifupdown/eth0-ipv4-network string 1.2.3.0
##live-debconfig live-debconfig/ifupdown/eth0-ipv4-broadcast string 1.2.3.255
##live-debconfig live-debconfig/ifupdown/eth0-ipv4-mtu string 1500
##live-debconfig live-debconfig/ifupdown/eth0-ipv4-post-up string post-command
 
# For static configuration of DNS.
##live-debconfig live-debconfig/ifupdown/nameserver-addresses string 5.6.7.8 9.10.11.12
##live-debconfig live-debconfig/ifupdown/nameserver-domain string example.com
##live-debconfig live-debconfig/ifupdown/nameserver-search string lan example.com
##live-debconfig live-debconfig/ifupdown/nameserver-options string debug
 
# ### Users (passwd)
live-debconfig live-debconfig/passwd/shadow boolean true
live-debconfig live-debconfig/passwd/root-login boolean true
live-debconfig live-debconfig/passwd/make-user boolean false
live-debconfig live-debconfig/passwd/root-password string toor
live-debconfig live-debconfig/passwd/root-password-again string toor
 
# FUSEAU HORAIRE
tzdata tzdata/Areas select Europe
tzdata tzdata/Zones/Etc select UTC
tzdata tzdata/Zones/Europe select Paris

Depuis peu, il n'est plus possible d'utiliser un preseed-file avec le template Debian sauf à utiliser le package « lxc » qui se trouve dans le dépôt « experimental » de Debian. Voir : 0.9.0~alpha3-2+deb8u1 : “lxc” package : Debian .

Scripts

Ensuite, il faut des scripts pour automatiser la création, le démarrage, l'arrêt et l'éventuelle destruction des conteneurs. Ci-dessus, vous trouverez les scripts que j'utilise. Ce n'est pas du grand art mais ça fait ce qui doit être fait.

Commun

Commun est un fichier qui sert à positionner les variables et les tests utiles à plusieurs scripts comme le nom de tous les conteneurs de la maquette LXC ou vérifier les droits.

MACHINES="R1 R2 R3 R4"
 
if [ $USER != "root" ]
then
	echo "Vous devez exécuter ce script en étant root."
	exit 1
fi
Créer

Pour que vous puissiez comprendre ce script, je dois vous expliquer comment je hiérarchise les différents éléments qui permettent de créer la maquette.

Racine de la maquette
|-- network
|-- routing
|-- scripts
|-- tests

Network contient des fichiers nommés « config.$NOM_DU_ROUTER ». Chacun d'entre eux contient les instructions qui permettent de mettre en place le réseau sous LXC (« lxc.network.type », « lxc.network.ipv4 », « lxc.network.hwaddr », ...). Elles seront ajoutées au fichier « config » du conteneur correspondant lors de la création de la maquette. C'est aussi dans ce dossier que je stocke un fichier « hosts » qui fait la correspondance entre le nom de mes machines (voir des interfaces quand j'ai besoin d'une telle granularité (exemple : « 198.18.0.1 eth0.r1.local. »)) et leur adresse IP "principale" (une loopback, par exemple).

Routing contient, quand j'en ai besoin, les fichiers de configuration de Quagga. Exemple : bgpd.conf, ospfd.conf, zebra.conf, ... Tous sont suffixés avec le nom du conteneur LXC sur lequel devra être mis le fichier. Exemple : « zebra.conf.R1 ». Ils seront copiés dans le dossier /etc/quagga lors de la création de la maquette.

Scripts contient tous les scripts shell de gestion de la maquette. Typiquement ceux que je suis en train de vous présenter. :P

Tests contient des scripts ou des binaires qui permettent d'effectuer des tests sur la maquette. Le script bash « pingall » (voir plus bas) est typiquement un de ces moyens de test. Ils seront copiés dans /root.

Ceci étant dit, voici le script le plus complet que j'ai écrit pour construire une maquette avec LXC :

#!/bin/bash
 
source commun
 
HERE=`pwd`
PARENT=$HERE/..
 
for i in $MACHINES
do
	# Création du conteneur
	lxc-create -n $i -t debian -- --preseed-file=$HERE/preseed-file
 
 
	# Proc doit être en RW pour permettre l'activation de l'ip_forward
	sed -i 's/proc proc proc ro/proc proc proc rw/' /var/lib/lxc/$i/config
 
 
	# Pour que SSH accepte les connexions
	sed -i 's/UsePAM yes/UsePAM no/' /var/lib/lxc/$i/rootfs/etc/ssh/sshd_config
 
 
	# Mise en place du réseau
	head -n-6 /var/lib/lxc/$i/config > /var/lib/lxc/$i/config.tmp
	mv /var/lib/lxc/$i/config.tmp /var/lib/lxc/$i/config
	cat $PARENT/network/config.$i >> /var/lib/lxc/$i/config
 
	# Toutes les interfaces sont sur le même bridge donc les réponses ARP ne venant 
	# pas de la bonne interface peuvent être gênantes. Pour eviter ça, on active arp_filter
	echo "net.ipv4.conf.all.arp_filter = 1" >> /var/lib/lxc/$i/rootfs/etc/sysctl.conf
	echo "net.ipv4.conf.default.arp_filter = 1" >> /var/lib/lxc/$i/rootfs/etc/sysctl.conf
 
	intSurCeRouteur=`grep -Eo "eth[0-9]" /var/lib/lxc/$i/config`
 
	for j in $intSurCeRouteur
	do
			echo "net.ipv4.conf.$j.arp_filter = 1" >> /var/lib/lxc/$i/rootfs/etc/sysctl.conf
	done
 
 
	# Associations IP<->nom
	cat $PARENT/network/hosts >> /var/lib/lxc/$i/rootfs/etc/hosts
 
 
	# Routing (on veut zebra + ospf + bgp)
	cp $PARENT/routing/zebra.conf.$i /var/lib/lxc/$i/rootfs/etc/quagga/zebra.conf
	cp $PARENT/routing/ospfd.conf.$i /var/lib/lxc/$i/rootfs/etc/quagga/ospfd.conf
	cp $PARENT/routing/bgpd.conf.$i /var/lib/lxc/$i/rootfs/etc/quagga/bgpd.conf
	sed -i 's/^zebra=no/zebra=yes/' /var/lib/lxc/$i/rootfs/etc/quagga/daemons
	sed -i 's/^ospfd=no/ospfd=yes/' /var/lib/lxc/$i/rootfs/etc/quagga/daemons
	sed -i 's/^bgpd=no/bgpd=yes/' /var/lib/lxc/$i/rootfs/etc/quagga/daemons
 
 
	# Forward
	sed -i 's/#net.ipv4.ip_forward=1/net.ipv4.ip_forward=1/' /var/lib/lxc/$i/rootfs/etc/sysctl.conf
 
 
	# Tests
	cp $PARENT/tests/pingall.sh /var/lib/lxc/$i/rootfs/root/
	chmod u+x /var/lib/lxc/$i/rootfs/root/pingall.sh
done
Lancer

On met en place ce qu'il faut (cgroups, création du bridge, on ne drop pas les paquets qui passent d'un conteneur à l'autre, ..) puis on lance les conteneurs et on vérifie qu'ils sont bien lancés.

#!/bin/bash
 
source commun
 
mount -t cgroup none /sys/fs/cgroup
 
brctl addbr br0 
ip link set up dev br0
 
iptables -P FORWARD ACCEPT
ip6tables -P FORWARD ACCEPT
 
for i in $MACHINES
do
	lxc-start -d -n $i
done
 
sleep 10
 
for i in $MACHINES
do
	echo "$i :"	
	lxc-info -n $i
	echo -e "\n"
done

Alors oui, ce script causera l'affichage d'erreurs lors d'utilisations successives car il n'y a pas de contrôles et que le bridge existe déjà et que les cgroups sont déjà montés. Rien de grave donc, juste trois lignes de textes sur la console. :P

Stopper

On arrête les conteneurs et on vérifie qu'ils ont bien été arrêtés.

#!/bin/bash
 
source commun
 
for i in $MACHINES
do
	lxc-stop -n $i
done
 
sleep 10
 
for i in $MACHINES
do
	echo "$i :"	
	lxc-info -n $i
	echo -e "\n"
done

Je sens que je vais me faire troller sur l'utilisation de « lxc-stop » qui fait un arrêt violent des conteneurs en lieu et place de « lxc-halt » qui laisse les conteneurs s'éteindre par eux-mêmes. L'explication est simple : sur chacune de mes maquettes, lxc-halt fonctionne pour quelques conteneurs très minoritaires même en attendant et je dois lancer lxc-stop ensuite ... Et comme je n'ai remarqué aucun dégât en utilisant uniquement lxc-stop ... bah j'utilise uniquement lxc-stop.

Détruire

Permet de détruire tous les conteneurs LXC de la maquette.

#!/bin/bash
 
source commun
 
for i in $MACHINES
do
	lxc-destroy -n $i
done
Netem

Pour ajouter une latence calculée et différente entre chaque lien sans utiliser de distribution à la normale :

#!/bin/bash
 
# À lancer sur l'hôte, après que toutes les interfaces du lab 
# aient été créées ... soit environ start.sh + 10 secondes
 
modprobe sch_netem
 
# Un groupe de lignes = un routeur. Ici R1
tc qdisc add dev lxc_R1_R2 root netem delay 1.180890283ms
tc qdisc add dev lxc_R1_R3 root netem delay 2.3495148631ms
 
# R2
tc qdisc add dev lxc_R2_R1 root netem delay 1.180890283ms
 
# R3
tc qdisc add dev lxc_R3_R1 root netem delay 2.3495148631ms
tc qdisc add dev lxc_R3_R4 root netem delay 0.8994545122ms
 
# R4
tc qdisc add dev lxc_R4_R3 root netem delay 0.8994545122ms

Pour appliquer d'autres caractéristiques (perte, corruption) sur un ou plusieurs liens, il suffit d'ajouter des lignes à ce script.

Pingall

Pour vérifier que le réseau de mes maquettes est opérationnel, j'utilise, en premier lieu, un bête script qui ping toutes les adresses IPs allouées (en utilisant les noms, comme ça, on teste aussi la validé du fichier /etc/hosts ;) ) et qui s'arrête à la première erreur rencontrée.

J'utilise souvent une loopback adressée sur chaque routeur. Cela permet de considérer les routeurs comme des destinations, ce qui est toujours utile (qui à dit MPLS ? :) ). Cela permet aussi "d'accrocher" les protocoles de routage dynamiques (exemple : BGP et « update-source ») dessus.

#!/bin/bash
 
INTERFACES_PHY="eth0.r1.local eth1.r1.local eth0.r2.local eth0.r3.local eth1.r3.local eth0.r4.local "
 
INTERFACES_LO="lo.r1.local lo.r2.local lo.r3.local lo.r4.local"
 
 
echo -e "\033[0;32m\n\n---------- Testons d'abord chaque lien ----------\n\033[0;39;49m"
j=0
for i in $INTERFACES_PHY
do
	ping -c 2 -w 2 $i
 
	if [ $? -ne 0 ]
	then
		echo -e "\033[0;31m\n\n---------- ÉCHEC ----------\n\033[0;39;49m"
		exit 5
	fi
 
	echo -e "\n"
done
 
 
echo -e "\033[0;32m\n\n---------- Testons ensuite chaque loopback ----------\n\033[0;39;49m"
j=0
for i in $INTERFACES_LO
do
	ping -c 2 -w 2 $i
 
	if [ $? -ne 0 ]
	then 
		echo -e "\033[0;31m\n\n---------- ÉCHEC ----------\n\033[0;39;49m"
		exit 5
	fi
 
	echo -e "\n"
done
 
 
echo -e "\033[0;32m\n\n---------- SUCCÈS ----------\n\033[0;39;49m"
exit 0
faq

À la recherche d’un MUA

Il y a 2 mois, je décidais d'utiliser exclusivement mes serveurs de mails personnels ou ceux d'entités de confiance. Adieu donc les Hotmail, Gmail, FAI, ... Et vous verrez plus loin que mon choix est antérieur au NSA-gate. ;) Histoire de tout faire en une seule fois, j'ai harmonisé les boîtes mail restantes et notamment le protocole de consultation (IMAP partout) et je me suis intéressé aux différentes manière de trier mes mails. Et, forcement, j'ai décidé de comparer les clients mail (MUA) existants pour voir si un autre MUA conviendrait mieux à mes besoins que Icedove (aka Thunderbird chez Debian). Je vais tenter de résumer tout ça dans ce billet bordelique.

Si vous êtes tombé sur ce billet alors que vous cherchez à mettre en place votre propre serveur mail, je vous redirige vers la partie « Courrier électronique » de wiki.auto-hebergement.fr et vers Postfix tranquille chez Karchnu pour l'installation basique et vers Sécuriser un serveur de mail chez Tetaneutral.net pour les techniques avancées.

Table des matières

FAQ

Avant d'entrer dans le vif du sujet, je vais répondre, de manière publique, aux deux questions que l'on m'a le plus souvent posées :

  • « Comment as-tu réussi la transition si vite et à faire en sorte que les gens ne t'écrivent plus que sur les nouvelles adresses ? ». Je n'ai pas fait la transition en seulement deux petits mois :
    • J'ai prévenu depuis janvier 2013 qu'à partir de juillet 2013, je ne recevrai plus les mails sur les anciennes adresses. Et j'ai été ferme sur le fait que je n'irai pas sur le webmail les lire. M'envoyer un mail aux anciennes adresses, c'est perdu ! Pour le reste, à savoir les mailing-list (liste de discussion) auxquelles je suis abonné et les entreprises/administrations/associations auxquelles j'ai donné mon adresse, j'ai effectué le changement d'adresse sur leur site web dès janvier et cela s'est très bien passé ... sauf pour 2 banques (une personnelle, l'autre pour une association) dans lesquelles il faut prendre un rendez-vous avec un conseiller pour changer une adresse mail ... Oui, en 2013, oui, oui, oui !
    • Entre janvier et juillet, à chaque fois que j'ai reçu un mail sur ce qui allait devenir une ancienne adresse, je répondais au mail, en indiquant, dès le début du mail, qu'il ne fallait plus utiliser cette adresse et en positionnant un en-tête « reply-to » (Réponse à) contenant la nouvelle adresse.

    Bilan : seulement 2 personnes n'ont pas compris et se sont plaintes récemment que je n'ai pas répondu à leurs mails ...

  • « Tu ne perds pas de mails ? ». Quasiment non. J'ai demandé à un maximum de personnes, qui utilisent des fournisseur d'adresses mail bien différents, de tester et il s'avère que le seul fournisseur chez qui mes mails tombent dans le dossier SPAM, c'est le casse-couille de la profession, à savoir, Hotmail ! Cela ne concerne que le premier mail, tant que la personne qui possède l'adresse Hotmail ne vous a jamais répondu ou écrit de sa propre initiative. Non, je ne suivrai pas leur procédure pour me faire autoriser : le mail est normalisé, je n'ai pas à suivre les instructions d'une entreprise particulière pour envoyer des mails ! Je pense avoir mis en place suffisamment de procédés normalisés (SPF, DKIM, reverse v4/v6 corrects, TLS, ...) pour que l'on se doive de me foutre la paix !

Choix d'un MUA

Je cherchais un MUA qui réunit ces quelques critères :

  • Logiciel libre.
  • Un client lourd, pas un webmail.
  • KISS. Je n'ai pas besoin d'avoir un agenda intégré, j'ai déjà un agenda. Je n'ai pas besoin d'avoir un gestionnaire de TODO intégré. Bref, je veux juste du mail.
  • Support de la fonctionnalité IDLE d'IMAP. Cela permet au client d'indiquer au serveur qu'il souhaite être notifié en temps-réel de l'arrivée de nouveaux mails.
  • Stockage des mails au format Maildir. Un mail par fichier par opposition à Mailbox avec lequel un fichier stocke toute une boîte. Maildir est réputé plus efficaces sur un volume moyen de mails. Mais surtout, un mail par fichier permet une sauvegarde impeccable et une restauration facile quand le MUA pète les plombs et dégage un mail ou mélange les en-têtes d'un mail avec le contenu d'un autre ... Si, si, si, ces deux cas me sont déjà arrivés avec Thunderbird. Heureusement, j'avais une sauvegarde mais du coup, les mails n'apparaissent plus dans le client. Pour les relire, il faut que j'ouvre la sauvegarde. Et c'est moyen-moyen.

L'ergonomie est un critère secondaire.

Si l'on exclut les clients mail dont l'implémentation de Maildir est un patch dont on ne sait pas si c'est stable ou non, il reste :

  • Mutt
  • KMail
  • Evolution
  • Icedove (aka Thunderbird)

Voici les avantages et les inconvénients que j'ai trouvé à chaque client.

Mutt :

  • + Je l'utilise déjà mais je souhaite quand même utiliser un autre client en parallèle le temps de finir d'apprendre les combinaisons de touches les plus utiles au quotidien. Histoire d'être efficace et surtout histoire de ne pas faire d'erreurs de manip' du genre « oooops, tiens, cette boite mail était vide avant ? ». :P
  • + KISS.
  • + Contrairement à tous les autres MUA, l'affichage des nouveaux mails est efficace : ouvrir un dossier et pouf, la sidebar affiche tous les nouveaux messages dans tous les dossiers. Avec Icedove/Evolution, je suis obligé de survoler l'intégralité des dossiers pour voir les nouveautés dans les dossiers.
  • + Léger ... Il ne prend pas 95 % du CPU pendant plusieurs dizaines de secondes pour relever une BAL.
  • + Maildir par défaut (voir dans Mutt/cache/BAL/).
  • - Je suis le seul avec qui Mutt segfault parfois. :(

KMail :

  • - Bien que les devs et la doc indiquent que KMail supporte IMAP IDLE, je n'ai pas trouvé ça fonctionnel.
  • - L'ergonomie de l'interface est vraiment, vraiment mauvaise. Pareil pour la mise en place d'un compte ... L'utilisation au quotidien est juste affreuse.

Evolution :

  • - N'est pas KISS : il fait aussi TODO, agenda, ...
  • - Pour voir si j'ai de nouveaux mails dans mes dossiers, je suis obligé de survoler l'intégralité des dossiers pour voir les nouveautés dans les dossiers.
  • + Maildir.
  • - Niveau ergonomie, on n'est pas loin de KMail ... Notamment, quand le texte d'un mail dépasse la fenêtre, impossible de scroller, il faut forcement utiliser l'ascenseur ou les flèches du clavier ...
  • - Il reste quelques petits bugs, par-ci, par-là. Il y a des paramètres (mapping/port d'un serveur par exemple) qui sautent sans raison lors d'un re-démarrage du logiciel donc il faut insister plusieurs fois pour que ça soit bien pris en compte.
  • - Evolution range les mails/paramètres/autre dans pas moins de 4 dossiers dans le home directory. Bon courage pour la sauvegarde ! Au moins Thunderbird et Mutt mettent tout dans un même dossier.

Icedove/Thunderbird :

  • + Le poids des habitudes : je n'ai connu que Thunderbird en client mail graphique donc y renoncer maintenant s'avère difficile.
  • + Quasiment KISS (mail + newsgroups sauf si des extensions sont installées (iceowl pour faire agenda, par exemple).
  • - Pour voir si j'ai de nouveaux mails dans mes dossiers, je suis obligé de survoler l'intégralité des dossiers pour voir les nouveautés dans les dossiers. ÉDIT du 19/09/2013 à 20h05 : Lire les commentaires de JB, le pourquoi du comment y est tout bien expliqué. Solution provisoire : mettre « mail.server.default.check_all_folders_for_new » à « true », solution définitive à long terme : IMAP NOTIFY (RFC 5465). Fin de l'édit
  • - L'implémentation de Maildir est récente, activable via de la bidouille dans les fichiers de configuration. Il y a eu des bugs importants ces derniers temps (déplacement foireux (l'original n'est pas effacé), une mise à la corbeille foireuse) et il en reste encore d'après le bugtracker (même si certains sont plus des PEBKAC que des bugs). Vu qu'elle est expérimentale, cette implémentation peut subire des changements radicaux (ie : les devs peuvent penser « ceux qui l'activent le font en connaissance de cause donc on peut se permettent de casser la compatibilité par la suite, les utilisateurs sont des beta-testeurs, ils s'attendent à perdre éventuellement des mails »).
  • - Lourdeur : 95 % du CPU pendant plusieurs dizaines de secondes pour juste parler IMAP ?! O_O

En fait, je me suis rendu compte que la première partie de la devise de Mutt, « All mail clients suck. », est vraie.

En conséquence, en attendant de savoir manier totalement Mutt pour mes usages quotidiens, j'ai choisi de continuer à utiliser Icedove/Thunderbird sans Maildir. En ce qui concernant les boîtes mail que j'auto-héberge (la majorité), j'ai une sauvegarde issue du serveur donc elle est au format Maildir. Pour les autres, j'ai la sauvegarde du dossier Icedove donc Mailbox mais sauvegarde quand même.

Tri du courrier

Il existe plein de mécanismes différents pour trier et filtrer ses mails.

Chaque MUA permet de le faire avec sa syntaxe précise.

Il est aussi possible d'utiliser des langages côté serveur. Cela permet d'avoir les mêmes règles de tri/filtrage quel que soit le client utilisé (client lourd, webmail, ...) puisque les mails sont triés sur le serveur et non plus par le client. Et c'est portable (si l'on change de MUA, que l'on oublie de sauvegarder sa config, ...). Il existe plein de mécanisme côté serveur : procmail, maildrop, Sieve, ...

Je me sers de Sieve, via Dovecot LDA, sur un de mes serveurs de mail et de procmail sur les 2 autres. Je m'en sers exclusivement pour déplacer un mail dans un dossier en fonction d'un critère : comparaison de l'en-tête « List-Id » pour identifier un mail d'une liste de discussion, comparaison du delimiteur dans l'adresse mail du destinataire pour identifier l'origine, une communauté, un service, ... ou pour supprimer les mails envoyés par des réseaux asociaux.

Pour les avantages et les inconvénients de procmail :

  • + Offre de grandes possibilités ;
  • - La courbe d'apprentissage est raide ;
  • - N'est pas normalisé.
  • - Pour modifier les règles, il faut avoir accès au serveur.

Pour les avantages et les inconvénients de Sieve :

  • + Normalisé. Plusieurs implémentations sont disponibles (Dovecot, Cyrius, ...) ;
  • + Facilité de prise en main / human-readable ;
  • - Moins de possibilités pour les utilisateurs expérimentés et exigeants.
  • + Pour modifier les règles, on peut utiliser Manage Sieve, un protocole dédié. Une extension Thunderbird existe pour cela mais je ne sais pas ce qu'elle vaut : j'ai un shell sur mes serveurs persos, quand même :P.

Utiliser procmail et/ou Sieve

procmail

Pour utiliser procmail avec Postfix, il suffit de rajouter ce qui suit au main.cf :

mailbox_command = /usr/bin/procmail -f- -a $USER -a $EXTENSION

« $EXTENSION » permet de fournir le texte après le délimiteur (exemple : toto dans guigui+toto@) à procmail. Cela permet d'écrire des règles qui matcheront cet élément. Source : Postfix Configuration Parameters - Mailbox command. ÉDIT du 19/09/2013 à 19h35 : Avant de me faire troller, je précise que le délimiteur n'est pas forcement le caractère « + » et qu'il peut être changé. Avec Postfix, il faut voir ça avec la directive de configuration « recipient_delimiter ». Fin de l'édit

Ensuite, il faut rajouter les instructions générales et les recipes (les règles) dans un fichier (.)procmailrc. Il peut se trouver dans /etc/ et il s'appliquera en conséquence à toutes les boîtes mail hébergées sur le système. Il peut se trouver dans le home directory (.procmailrc) d'un utilisateur auquel cas il s'appliquera uniquement à la boîte aux lettres de cet utilisateur. Si les deux existent, procmail exécutera d'abord celui contenu dans /etc/ puis celui contenu dans le home directory. Source : man procmail (« If no rcfiles and no -p have been specified on the command line, procmail will, prior to reading $HOME/.procmailrc, interpret commands from /etc/procmailrc (if present). »).

Du coup, je me sers de /etc/procmail pour indiquer des instructions générales (log, dossier par défaut si le dossier précisé dans une règle n'existe pas, ...) dans /etc/procmailrc. Les règles se trouvent dans mon home directory. Voici, par exemple, le contenu de mon fichier /etc/procmailrc :

SHELL=/bin/false
PATH=/bin:/usr/bin:/usr/local/bin
 
LOGFILE=/var/log/mail/procmail.log
VERBOSE=no
 
DROPPRIVS=yes
 
LOGNAME=$USER
MAILDIR=$HOME/Maildir
DEFAULT=$HOME/Maildir/new
ORGMAIL=$MAILDIR/new

Attention :

  • Sur le web, j'ai déjà vu des procmailrc contenant « DEFAULT=$HOME/Maildir/cur ». Cela me semble être incorrect. Le format Maildir indique que le MTA/MDA place le mail dans tmp durant sa réception puis le déplace dans new dès la réception complète. Seul le MUA déplace le mail dans cur. Or, procmail n'est pas un MUA. Source : Maildir sur Wikipedia.
  • Procmail s'enlève les droits dès qu'il parse l'instruction « DROPPRIVS ». Donc la mettre avant « LOGFILE » empêche procmail de logger (dans /var/log/* j'entends).

Quelques exemples de règles :

DELIM = $2
 
# Les listes de discussion de la FFDN seront rangées dans le dossier FFDN 
:0:
* ^List-Id:.*lists.ffdn.org
$HOME/Maildir/.FFDN/
 
# Tout ce qui arrive sur l'adresse guigui+toto@ va dans le dossier toto
:0:
* DELIM ?? ^^toto^^
$HOME/Maildir/.toto/

## Pour dégager les réseaux asociaux
# LinkedIn - premier mail
:0H:
* ^Subject:.*Check out my profile on LinkedIn.*
/dev/null

# LinkedIn - relances
:OH:
* ^From: .*invitations@linkedin.com
/dev/null

## Fin dégager les réseaux asociaux

Pour apprendre à écrire vos règles procmail, je vous recommande les ressources suivantes :

Sieve avec Dovecot LDA

Pour utiliser Dovecot LDA avec Postfix, il suffit de rajouter ce qui suit au main.cf :

mailbox_command = /usr/lib/dovecot/dovecot-lda -f "$SENDER" -a "$RECIPIENT" -d "$USER"

Il faut également ajouter ce qui suit dans /etc/dovecot/dovecot.conf (ou dans les bons fichiers dans conf.d pour ceux qui veulent faire propre et version 2 compliant) :

protocol lda {
        mail_plugins = sieve
        postmaster_address = postmaster@votredomaine.example
}
 
plugin {
         sieve = ~/.dovecot.sieve
}

Source : The Perfect Server - OpenSUSE 12.2 x86_64 (Apache2, Dovecot, ISPConfig 3 - Page 4.

Ensuite, il ne reste plus qu'à placer les règles dans ~/.dovecot.sieve (qui est le fichier que nous avons indiqué dans la directive « sieve » dans dovecot.conf). Le log s'ajoute directement dans le log général, avec les tentatives de connexions IMAP/POP.

Quelques exemples de règles :

require ["fileinto", "envelope", "subaddress"];
 
# Les listes de discussion de la FFDN seront rangées dans le dossier FFDN 
if anyof (
	header :contains "List-Id" "lists.ffdn.org",   # Si le mail vient de la ML 
	header :contains "Subject" "[FFDN]"            # ou réponse hors-liste (fictif)
) {     
	fileinto "FFDN";
	stop; # Pas la peine de parcourir le reste des règles
}
 
# Tout ce qui arrive sur l'adresse guigui+toto@ va dans le dossier toto
if envelope :detail "to" "toto" {
	fileinto "toto";
	stop;
}

# Pour dégager les réseaux asociaux
if anyof (
       # LinkedIn (premier mail + relances)
       header :is ["Subject"] "Check out my profile on LinkedIn",
       address :is ["From"] "invitations@linkedin.com"
)
{
       discard;
       stop;
}

Pour apprendre à écrire vos règles Sieve, je vous recommande les ressources suivantes :

Trucs & astuces

Icedove/Thunderbird

Si vous utilisez les délimiteurs dans votre adresse mail (exemple : toto+commerçantA@votredomaine.example ou toto+listeDeDiscussionA@votredomaine.example) pour trier votre courrier plus facilement et savoir qui a revendu votre adresse mail, vous pouvez avoir besoin d'envoyer des mails avec comme adresse d'expéditeur, l'adresse complète soit toto+listeDeDiscussionA@votredomaine.example et non pas uniquement toto@votredomaine.example. C'est typiquement le cas pour les listes de dicussions : le gestionnaire de la liste (sympa, mailman, ...) n'acceptera vos mails que s'ils contiennent l'adresse d'expéditeur indiquée lors de votre inscription à la liste.

Evidemment, vous n'allez pas créer autant de compte dans votre MUA que d'adresse mail différente : Thunderbird (comme les autres MUA) permet de lier plusieurs identités à un compte mail. Voir : Multiple identities per e-mail account.

Paramètres des comptes -> Sélectionner le compte concerné -> Gérer les identités -> Ajouter -> saisir juste le nom et l'adresse mail avec le délimiteur -> Ok *3.

Créer des adresses mail temporaires auto-hebergées

Voir : Service d'adresse de redirection temporaire. Je trouve ça excellent !

Le mécanisme d'alias vous permet de créer des adresses temporaires pour votre domaine. Les alias virtuels permettent de créer des alias qui sortent de votre domaine, pour des amis, une petite communauté, ... Il n'est pas nécessaire d'utiliser le script PHP (sauf si vous ouvrez votre service à tout le monde, bien sûr) : il suffit d'ajouter/supprimer des alias.

Mutt

Sous Mutt, impossible de se passer de mutt-sidebar. Sous Debian, c'est le package mutt-patched qu'il faut installer.

Pour vous faire une config Mutt qui ne craint pas trop, je vous recommande ces deux ressources : Desktop in a shell: mutt et Mutt sur le wiki Archlinux.

Pour utiliser plusieurs adresses mail sans les rediriger sur une seule, il suffit d'utiliser les profiles. Je ne sais pas pourquoi je lis à droite et à gauche « hola, c'est compliqué, utilises des alias plutôt » ... Le commutateur « -F » permet de préciser un muttrc différent de ~/.muttrc. Il suffit donc de créer autant de profiles (et donc de fichiers) que de boîtes aux lettres différentes, contenant chacun les informations nécessaires pour utiliser une BAL (paramètres généraux, IMAP, SMTP, ...). Tout en utilisant la directive « source » pour inclure un fichier contenant les directives communes à tous les comptes mail. Il suffit ensuite d'utiliser screen et un alias. Exemple : l'alias « screen-mail » ouvre X onglets et lance un « mutt -F profile-X » dans chaque. On peut aussi utiliser uniquement des alias : Kevin's Mutt Tips and Tricks - Mutt

international
rss
Bear
contact