Administration Linux

Fil des billets

Linux et le cas étrange des trous noirs TCP/IP

Sur Internet, certains sites peuvent être mystérieusement inaccessibles. Quelle est la cause de ces étranges trous noirs ?

Lors de l'établissement d'une connexion TCP, les 2 machines négocient la taille maximale des paquets échangés (le MTU). Cependant, elles ignorent les contraintes propres au chemin suivi.

Normalement, si un paquet envoyé est trop gros, le routeur concerné le signale, via un message ICMP, et la machine émettrice ajuste sa taille de paquets[1]

Cependant, ces messages ICMP sont parfois bloqués par des pare-feu paranoïaques. C'est là que les ennuis commencent. Le routeur jette le paquet et signale le problème avec un message ICMP. Ce message est bloqué par le pare-feu. C'est le trou noir. Les paquets continuent à être envoyées, mais sont perdus à chaque fois.

Une solution à ce problème est décrite par le RFC 4821. Afin de déterminer la taille optimale des paquets, l'émetteur va envoyer des sondes, c'est-à-dire des paquets un peu plus gros que les autres, afin de rechercher la taille idéale. Si la sonde ne passe pas, mais que le reste du trafic passe, c'est que la taille de la sonde est trop grosse. Si elle passe, sa taille servira de base aux paquets suivants.

Linux dispose de ce mécanisme depuis le noyau 2.6.17, activable via l'option tcp_mtu_probing. Cependant, cette première mise en œuvre n'est pas très subtile.

Lorsque la détection démarre, on bascule à une taille de segment par défaut (tcp_base_mss[2]). Si rien ne passe, on divise cette taille par 2. Si les paquets passent, on essaie d'envoyer des sondes en doublant la taille des paquets.

Avec l'option tcp_mtu_probing=1, on ne déclenche ce mécanisme que si l'on détecte un trou noir (un blocage du trafic).

Par exemple, avec une taille de paquet négociée de 1460, on détecte un trou noir. La taille des paquets est passée à 512 (ça passe), puis on envoie une sonde à 1024 (ça passe encore), le trafic passe à 1024. On devrait envoyer une sonde à 2048, mais cela dépasse la capacité de la carte Ethernet (sans Jumbo Frames). Fin de la recherche. Le trafic reste sur une taille de paquet de 1024. Même si la taille optimale était de 1300.

Cela dit, c'est un compromis acceptable pour éviter les blocages. Mieux vaut un débit un peu dégradé qu'un blocage du trafic.

On peut paramétrer ce mécanisme pour être utilisé en permanence avec l'option tcp_mtu_probing=2.

Inutile de le dire, c'est une très très mauvaise idée.

Dans ce second cas, dès l'établissement du flux, on bascule en mode détection de MTU. Donc, on commence à la taille définie par tcp_base_mss, puis on envoie des sondes en doublant la taille à chaque fois. Pour un réseau Ethernet, on va se retrouver systématiquement avec des paquets dont la taille sera limitée à 1024 (au lieu de 1460).

Avec Linux 4.1, ce mécanisme a été sérieusement amélioré. La taille n'est plus doublée, mais les sondes sont utilisée pour réaliser une recherche par dichotomie de la taille idéale, ce qui permet de se rapprocher assez rapidement de la taille idéale. De plus, la taille de segment de base (tcp_base_mss) passe à 1024.

Quel paramétrage adopter ?

  • Activez systématiquement ce mécanisme en mode détection de trou noir (tcp_mtu_probing=1).
  • Pour les noyaux < 4.1, passez la taille de base (tcp_base_mss) à 1024.

Avec les noyau 4.1 et plus, le mode tcp_mtu_probing=2 est beaucoup moins pénalisant, mais pas forcément recommandé (il y a une dégradation de la taille des paquets en début de connexion). Dans tous les cas, le mode tcp_mtu_probing=1 permet de se protéger des trous noirs sans prendre de risque.

Notes

[1] C'est le mécanisme de découverte automatique du MTU du chemin (Path MTU discovery), décrit par le RFC 1191. Ce mécanisme est activé par défaut sous Linux.

[2] La taille de segment (MSS) est la taille des données du paquet TCP. La valeur de tcp_base_mss est 512 par défaut. Cette valeur passe à 1024 avec le noyau 4.1. Une valeur de tcp_base_mss de 1024 devrait vous donner un MTU initial de 1064 (1024 + 20 pour l'en-tête IP + 20 pour l'en-tête TCP). Comme rien n'est simple, sur les anciens noyaux Linux, il faut également y ajouter la taille des options TCP.

Conversion des fins de lignes entre Unix et Windows

C'est la méthode la plus simple que j'aie trouvée. Elle nécessite de disposer d'un awk autorisant l'emploi d'expressions rationnelles comme séparateur d'enregistrements (RS). De plus, elle marche sur des fichiers comportant à la fois des fins de lignes Unix et DOS, sans ajouter de lignes blanches parasites.

L'utilisation de la variable BINMODE[1] est optionnelle. Sur les awk compatibles, elle désactive les conversions implicites des fins de lignes.

Cette procédure a été testée avec gawk, mawk et busybox awk[2]. Elle ne marche pas avec le awk original.

Convertir des fins de lignes Unix (LF) en fins de lignes DOS (CR-LF)

Utiliser la commande awk suivante :

gawk -v BINMODE=3 -v RS='(\r\n|\n)' -v ORS='\r\n' '{ print }'

Par exemple :

gawk -v BINMODE=3 -v RS='(\r\n|\n)' -v ORS='\r\n' '{ print }' fichier.unix.txt > fichier.dos.txt

Pour vérifier le résultat (en remplaçant u8 par le code du codage utilisé si vous n'êtes pas en UTF-8[3]) :

# Sans conversion
echo -e 'essai\nessai\r\nessai' | recode u8..dump-with-names

# Avec conversion
echo -e 'essai\nessai\r\nessai' | gawk -v BINMODE=3 -v RS='(\r\n|\n)' -v ORS='\r\n' '{ print }' | recode u8..dump-with-names

Convertir des fins de lignes DOS (CR-LF) en fins de lignes Unix (LF)

Utiliser la commande awk suivante :

gawk -v BINMODE=3 -v RS='(\r\n|\n)' -v ORS='\n' '{ print }'

Par exemple :

gawk -v BINMODE=3 -v RS='(\r\n|\n)' -v ORS='\n' '{ print }' fichier.dos.txt > fichier.unix.txt

Pour vérifier le résultat (en remplaçant u8 par le code du codage utilisé si vous n'êtes pas en UTF-8) :

# Sans conversion
echo -e 'essai\nessai\r\nessai' | recode u8..dump-with-names

# Avec conversion
echo -e 'essai\nessai\r\nessai' | gawk -v BINMODE=3 -v RS='(\r\n|\n)' -v ORS='\n' '{ print }' | recode u8..dump-with-names

Notes

[1] Cf. la documentation de gawk et la documentation de mawk.

[2] L'option -v BINMODE=3 est inutile avec busybox awk.

[3] Par exemple l1 pour Latin 1 (ISO-8859-1) et l9 pour Latin 9 (ISO-8859-15), lancez recode -l pour obtenir la liste complète des codages.

Modifier le navigateur par défaut sous Xubuntu (et tous les dérivés de Debian)

Sous Xubuntu, lorsque plusieurs choix existent pour la même commande, le logiciel utilisé par défaut sur la machine sera défini au travers du mécanisme des alternatives.

Il s'agit d'un mécanisme système de bas niveau. Les environnements de bureau comme XFCE ont leur propre mécanisme de sélection, mais qui ne couvre pas tous les cas.

Le mécanisme des alternatives fonctionne de la façon suivante : par défaut, une note est attribuée à chaque alternative pour une commande donnée. L'application ayant la meilleure note sera sélectionnée par défaut. Il est ensuite possible à un administrateur de sélectionner manuellement une autre commande moins bien noté (mais mieux adapté à ses besoins).

Il pourra s'agir de commandes réelles (quelle version de java doit-on utiliser lorsque l'utilisateur utilise la commande java) ou de commandes virtuelles (par exemple, une application appellera la commande x-www-browser pour invoquer le navigateur par défaut).

Pour définir manuellement le navigateur par défaut sous Xubuntu, suivez la procédure suivante :

  • Lancez le « Configurateur d'alternatives » (dans « Tous les paramètres » sous Xubuntu).
  • La fenêtre de paramétrage des alternatives apparaît :

galternatives.png

  • Dans la liste de gauche (« Alternatives »), sélectionnez « x-www-browser », qui définit la commande servant à appeler le navigateur par défaut.
  • À droite (dans « Options »), apparaît la liste des navigateurs disponibles. Sélectionnez la navigateur voulu dans cette liste.
  • Fermez le « Configurateur d'alternatives ».

Gérer un wiki MoinMoin 1.9 sous Linux

Vous venez d'installer un wiki MoinMoin. Vous croyez en avoir fini. Et bien non, détrompez-vous, il vous reste beaucoup de choses à faire... Petit inventaire :

Limiter la durée des sessions

Par défaut, MoinMoin permet aux utilisateurs de conserver une session ouverte sans limite de durée. Ce n'est pas forcément une bonne idée en termes de sécurité.

Vous pouvez limiter la durée des sessions des utilisateurs connectés, en ajoutant le paramètre cookie_lifetime dans le fichier de paramétrage de votre wiki :

cookie_lifetime = (0, -4)

Ici, les sessions utilisateur sont limitées à 4 heures, ce qui est généralement suffisant.

Nettoyer les sessions obsolètes

L'administrateur est responsable du nettoyage des fichiers correspondant aux sessions. Pour cela, il doit programmer régulièrement la commande de nettoyage (1 fois par jour est une bonne fréquence).

Créez un fichier /etc/cron.daily/local-moin-nettoyage-sessions contenant la commande de nettoyage des sessions obsolètes :

/bin/su - compte_moin -c "/usr/bin/moin --config-dir=/chemin/vers/le/wiki --wiki-url=http:/monwiki.org.example/ maint cleansessions > /dev/null 2>&1"

(Cf. http://sourceforge.net/p/moin/mailm...)

Relancer le wiki une fois par jour

C'est l'option recommandée par les développeurs de MoinMoin pour limiter les impacts d'une éventuelle fuite mémoire.

Effectuer une rotation des journaux

En plus des journaux de MoinMoin, pensez également à programmer une rotation du fichier data/event-log, qui sinon va continuer sa course vers l'infini :

/chemin_vers_les_journaux_moin/*.log {
        copytruncate
        daily
        missingok
        rotate 7
        compress
        delaycompress
        notifempty
}

/chemin_vers_le_wiki/data/event-log {
	daily
	missingok
	rotate 7
        compress
        delaycompress
        notifempty
}

Pour aller plus loin

Jetez un œil sur la page Améliorer les performances de MoinMoin.

De l'art de reconstruire les mirroirs...

Ça y est, c'est fait, l'un des disques qui composait votre belle partition en RAID 1 vient de tomber.

Bon, ça devait bien arriver un jour, mais maintenant, on fait quoi ?

Pour l'exemple, je supposerai que vous disposez de deux partitions RAID 1 (md0 et md1), construite à partir des partitions 1 et 2 des deux premiers disques (sda1 et sdb1 pour md0, sda2 et sdb2 pour md1).

Identifier le disque

Pour commencer, il faut identifier le disque à l'origine de tout ça :

# cat /proc/mdstat
Personalities : [raid1] 
md0 : active raid1 sda1[0]
      31246272 blocks [2/1] [U_]

md1 : active raid1 sda2[0] sdb2[0]
      31246272 blocks [2/2] [UU]
   
unused devices: <none>

Le mirroir sda1 de md0 est toujours là, mais il est tout seul.

Jetons maintenant un œil au journal système :

# grep md0 /var/log/syslog
kernel: [  954.420430] md/raid1:md0: Disk failure on sdb1, disabling device.
kernel: [  954.420432] md/raid1:md0: Operation continuing on 1 devices.
mdadm[1948]: Fail event detected on md device /dev/md0, component device /dev/sdb1
mdadm[1948]: SpareActive event detected on md device /dev/md0, component device /dev/sdb1

Là, pas de doute, suite à un problème sur la partition sdb1, le système l'a enlevé de l'ensemble RAID.

Avant de continuer, récupérons le numéro de série du disque sdb :

# smartctl -i /dev/sdb
smartctl 5.39b 2010-05-11 r3120 [x86_64-unknown-linux-gnu] (local build)
Copyright (C) 2002-10 by Bruce Allen, http://smartmontools.sourceforge.net

=== START OF INFORMATION SECTION ===
Model Family:     SAMSUNG SpinPoint T166 series
Device Model:     SAMSUNG HD501LJ
Serial Number:    S0MUJ1MPC07654
Firmware Version: CR100-12
User Capacity:    500 107 862 016 bytes
Device is:        In smartctl database [for details use: -P show]
ATA Version is:   8
ATA Standard is:  ATA-8-ACS revision 3b
Local Time is:    Mon Aug 15 17:10:36 2011 CEST
SMART support is: Available - device has SMART capability.
SMART support is: Enabled

Connaître le numéro de série permettra d'être sûr de retirer le bon disque (au lieu de retirer le disque qui marche).

Retirer les partitions toujours actives

Le second ensemble RAID est toujours là. Il utilise une partition de notre disque défectueux.

Pour éviter les problèmes, nous allons la retirer :

# mdadm /dev/md1 --fail /dev/sdb2
# mdadm /dev/md1 --remove /dev/sdb2

Nous avons donc maintenant :

# cat /proc/mdstat
Personalities : [raid1] 
md0 : active raid1 sda1[0]
      31246272 blocks [2/1] [U_]

md1 : active raid1 sda2[0]
      31246272 blocks [2/1] [U_]
   
unused devices: <none>

Plus aucune partition n'utilise le disque sdb.

Démonter l'ancien disque et installer le nouveau

Là, à vous de jouer. Surtout, vérifiez avec le numéro de série que vous retirez bien le bon disque :-)

Partitionner le nouveau disque

Votre nouveau disque est en place.

Si vous aviez utilisé un partitionnement identique pour vos deux disques (ce que je vous recommande), il vous suffit maintenant de recopier la table de partition du disque sda sur le nouveau disque (sdb) :

# sfdisk -d /dev/sda | sfdisk /dev/sdb

Hum... Ne vous trompez pas de disque... Surtout pas...

Reconstruire le RAID

C'est presque fini. Pour terminer, il suffit d'insérer les nouvelles partitions dans l'ensemble RAID :

# mdadm /dev/md0 --add /dev/sdb1
# mdadm /dev/md1 --add /dev/sdb2

Jetons un œil au résultat :

# cat /proc/mdstat
Personalities : [raid1]
md0 : active raid1 sda1[0] sdb1[2]
      31246272 blocks [2/1] [U_]
      [>....................]  recovery =  0.0% (1024/31246272) finish=857.8min speed=8618K/sec

md1 : active raid1 sda2[0] sdb2[2]
      31246272 blocks [2/1] [U_]
              resync=DELAYED

unused devices: <none>

Il ne reste plus qu'à surveiller la reconstruction avec un petit :

# watch cat /proc/mdstat

Et voilà. C'est fini !

Enfin, quand même, si vous avez besoin de redémarrer, attendez la fin de reconstruction du RAID :-)

De l'art de surveiller les scripts...

Écrire un script, c'est bien, mais, si le script est lancé automatiquement, il est judicieux de disposer d'un journal permettant de s'assurer de son bon fonctionnement[1]...

J'ai mis en place il y a peu un script chargé d'apprendre à spamassassin à reconnaître des messages indésirables. Ce script lance la commande sa-learn, et pour savoir s'il fonctionne bien, je veux savoir ce qu'a affiché sa-learn.

La commande logger

Pour enregistrer un message dans le journal système (/var/log/syslog[2] sur une distribution Débian), on peut utiliser une commande très pratique : logger.

Par exemple :

logger -p user.info -t mon_script "Mon message"

fera apparaître le message suivant dans le journal système :

Jun 21 22:12:24 mon_serveur mon_script: Mon message
Un premier essai

Il suffit donc de prendre les messages affichés par sa-learn et de les envoyer à logger :

MA_SORTIE="$( sa-learn --spam "${FICHIER}" 2>&1 )"
logger -p user.info -t mon_script "Resultat : ${MA_SORTIE}"

Cependant, nous ne pouvons pas être sûr que sa-learn renverra une seule ligne[3].

La même chose... En mieux

Le plus simple serait de fusionner les lignes affichées par sa-learn avant de les enregistrer dans le journal[4].

Ce que l'on peut faire simplement une ligne de awk :

sa-learn --spam "${FICHIER}" 2>&1 | gawk -v ORS="" 'NR > 1 { print " -/- " } ; { print }'

Donc, au bout du compte, c'est tout simple :

RESULTAT_COMMANDE="$( sa-learn --spam "${FICHIER}" 2>&1 | gawk -v ORS="" 'NR > 1 { print " -/- " } ; { print }' )"
logger -p user.info -t mon_script "sa-learn => ${RESULTAT_COMMANDE}"

En deux lignes, on lance sa-learn et on renvoie son résultat dans le journal système.

Notes

[1] Les programmes informatiques sont comme ça, si personne ne les surveille, ils font n'importe quoi.

[2] Le fichier journal utilisé dépendra du paramétrage du démon syslogd, de la priorité choisie (ici, info) et de la catégorie du message (ici, user)

[3] En général, les messages des journaux systèmes sont sur une seule ligne, ce qui permet de les exploiter simplement avec des outils Unix comme grep et logcheck.

[4] Attention ! On parle ici de fusionner une ou deux lignes, pas plus.

Un écureuil, un éléphant et un peu de sel

Le module change_sqlpass de Squirrelmail permet de modifier un mot de passe conservé dans une base PostgreSQL.

Lors du paramétrage de ce module, il faut avoir la réponse à une question très technique, qui risque de vous laisser pantois : comment déterminer de grain de sel (salt en anglais) utilisé dans vos mots de passe.

Un grain de sel ?

Un peu d'histoire. Sous les systèmes de type Unix, les mots de passe sont chiffrés avec un chiffrement non réversible (autrement dit, on ne peut pas retrouver votre mot de passe à partir de sa version chiffrée).

Lorsque vous vous connectez, on chiffre le mot de passe que vous entrez et on le compare au mot de passe officiel (déjà chiffré). Pas besoin donc de savoir le déchiffrer.

Cependant, cette méthode simple présente plusieurs inconvénients. Si deux utilisateurs ont le même mot de passe, cela sera immédiatement visible en regardant la table des mots de passe. De même, il est très simple d'effectuer une recherche systématique de mots de passe, en chiffrant un à un tous les mots d'un dictionnaire et en les comparant aux mots de passe de tous les utilisateurs.

Pour mitiger ces problèmes, on s'est résolu à ajouter un grain de sel aux mots de passe. Chaque utilisateur se voit attribuer un code différent (le grain de sel) qui modifie le mot de passe chiffré obtenu et est conservé avec le mot de passe.

Pour vérifier le mot de passe, on prend donc le mot de passe de l'utilisateur, le grain de sel lu sur le mot de passe enregistré, on les utilise pour recréer un mot de passe chiffré et on les compare au mot de passe chiffré enregistré.

Avantage, 2 utilisateurs avec les même mots de passe auront des mots de passe chiffrés différents. Et une recherche exhaustive de mots de passe devra maintenant se faire utilisateur par utilisateur, ce qui sera significativement plus long (enfin, en théorie. l’existence d'immenses tables de mots de passes pré-calculées relativisant un peu cet avantage).

Bon, concrètement, on fait quoi ?

Revenons à change_sqlpass. Dans le cas d'un mot de passe chiffré avec PHPCRYPT, le grain de sel se trouve dans les 2 premiers caractères du mot de passe. Il faut donc utiliser une requête comme celle-ci pour récupérer le grain de sel :

$csp_salt_static = '';
$csp_salt_query = "SELECT substr( mot_de_passe_chiffre, 1, 2 ) FROM table_utilisateur WHERE nom = '%2'";

Dans le cas d'un mot de passe condensé avec MD5, aucun grain de sel n'est utilisé :

$csp_salt_static = '';
$csp_salt_query = '';

Enfin, dans le cas d'un mot de passe chiffré avec MD5CRYPT, le format du mot de passe chiffré est : $1$gain_de_sel$mot_de_passe_chiffré. On peut dont récupérer le grain de sel avec une requête du type :

$csp_salt_static = '';
$csp_salt_query = "SELECT split_part( mot_de_passe_chiffre, '$', 3 ) FROM table_utilisateur WHERE nom = '%2'";

(Un petit correctif de documentation a été remonté à l'équipe Squirrelmail.)

Des noisettes pour Squirrelmail

J'utilise pour gérer mon courrier un client de messagerie web, Squirrelmail. Celui-ci dispose d'un module d'extension, change_sqlpass qui permet de changer en ligne son mot de passe, si celui-ci est enregistré dans une base de données.

Le module annonce fièrement que ça dernière version est compatible avec PostgreSQL, qui est la base de données que j'utilise.

Génial, exactement ce que je cherchais !

Aussitôt dit, aussitôt fait, je vais chercher le module et je l'installe...

Installation... Relance... Essai...

Et là, déception, ça ne marche pas.

Petit tour du code, et là quelques surprises :

  • Les guillemets utilisé dans les requêtes SQL sont des guillemets doubles (ce qui marche avec MySQL, mais pas avec PostgreSQL).
  • Le mot de passe est passé à la moulinette escapeSimple[1] avant d'être chiffré. Avec MySQL, ça marchait bien, car le mot de passe était chiffré dans une requête SQL. Avec PostgreSQL, il faut utiliser une fonction externe de chiffrement avant de mettre le mot de passe en base. Là, c'est la cata., si le mot de passe contient, par exemple, un guillemet simple, celui-ci sera transformé, et le mot de passe enregistré ne sera pas celui que l'utilisateur a entré... Mauvaise surprise garantie à la reconnexion ^^
  • Petite cerise sur le gâteau (ce n'est pas forcément la faute du module), les caractères accentués ne passe pas. Par sécurité, il vaudrait mieux refuser ces mots de passe

Là, un léger doute m'étreint... Ce module a-t-il déjà été testé avec PostgreSQL... Peut-être pas ^^

Qu'à cela ne tienne, vous trouverez en annexe un petit correctif corrigeant ces problèmes (ce correctif, coupé en morceaux, a été dûment remonté au projet, avec un petit rapport d'anomalie en prime 1 2 3 4).

Ce correctif effectue les modifications suivantes :

  • Utilisation de guillemets simples dans le requêtes (attention, mettez à jour votre fichier config.php à partir de config.php.sample, faute de quoi ça ne marchera plus).
  • Utilisation de escapeSimple uniquement lorsque le mot de passe est envoyé tel quel dans une requête.
  • Ajout d'une option pour refuser les mots de passe contenant des caractères accentués (enfin, non ASCII).

Note

[1] Une fonction servant à modifier une chaîne de texte pour éviter qu'elle soit mal interprétée par une base de données

Jamais content

postfix/smtpd: sql_select option missing
postfix/smtpd: auxpropfunc error no mechanism available
postfix/smtpd: _sasl_plugin_load failed on sasl_auxprop_plug_init for plugin: sql
postfix/smtpd: sql_select option missing
postfix/smtpd: auxpropfunc error no mechanism available
postfix/smtpd: _sasl_plugin_load failed on sasl_auxprop_plug_init for plugin: sql

Depuis peu, un message intriguant vient polluer les journaux systèmes de mon mini-serveur.

Après quelques recherches, son origine est simple : j'avais installé le paquet libsasl2-modules-sql pour postfix. Il s'agit d'un module permettant d'authentifier les utilisateurs via une base SQL.

Récemment, j'ai arrêté de m'en servir.

Et, naïvement, j'avais pensé que supprimer le paramétrage servant à l'activer aurait suffit à s'en débarrasser.

Que nenni ! Tant qu'il est là, il reste actif, et s'il n'est pas paramétré, il se plaint.