Routeur debian read-only

Ca fait quelques années que j'utilise Debian + Linux pour faire des routeurs, avec des petites transformations. Au fil du temps j'en suis venu à modifier le système pour le faire tenir sur une clé USB (en read-only pour qu'elle ne défaille pas trop vite), parce que les accès disque sur un routeur c'est rare, et que quasiment tout peut tenir dans la RAM.

On obtient ainsi un routeur efficace souple et puissant, sur lequel l'upgrade est un simple changement de clé.

A l'occasion d'une Nième modification, j'ai fait cette petite doc.

On a besoin de - une Debian générique avec qemu/kvm (appelée ci-dessous «generic») - un disque ou une clé USB de 4 Go comme cible

Principes

Tout le système sera en read-only dans /, environ 3 Go.

Une petite partition (moins de 1 Go) sera montée en read-write, de temps en temps, sur /cfg. Elle contiendra les modifications locales de /etc par rapport à la la config de base qui elle est contenue dans /conf/base/etc.

A chaque boot, on montera tout ce qui est susceptible de voir des modifications (genre log, run, lock) dans des volumes en RAM.

Même /etc/ est monté en RAM, le contenu de /conf/base/etc est copié dedans et écrasé avec les fichiers locaux cd /cfg.

/cfg est démonté, / est remonté en read-only

Quand une modif est faite localement dans /etc, elle est sauvegardée dans /cfg soit par un appel explicite de «save_cfg -a», soit quand le shell root quitte.

Backups

En parallèle, le système est backupé classiquement par rsync depuis un serveur de fichiers.

Installation

Système

Je passe sur l'installation d'un système de base. Ici je suppose qu'on a déjà une image avec une Debian de base installée dessus à portée de la main et je ne m'intéresse à ce qui est spécifique à en faire un routeur en read-only.

Premièrement, un système de base sur la première partition.

On peut le faire par exemple a partir d'une image de VM qemu/kvm (voir https://giw3.gixe.net/wakka.php?wiki=DebianKvm) d'une debian de base, en la clonant.

TARGET_DISK=<chemin du volume>
fdisk -ul $TARGET_DISK
mount -o loop,offset=$((SECTOR_SIZE * PARTITION_START)) -t ext3 $TARGET_DISK /mnt 

A partir de là on va travailler depuis l'intérieur : chroot /mnt bash

Remplissage

On change le nom pour éviter de ne plus savoir ou on se trouve par la suite :

hostname NEW_NAME
echo NEW_NAME > /etc/hostname

on RAZ les règles persistences de nommage d'interfaces de udev, et on met en place la config qui va bien pour eth0 (à adapter selon les besoins bien entendu)…

cp /dev/null /etc/udev/rules.d/70-persistent-net.rules
cat <<EOF >/etc/network/interfaces
auto lo
iface lo inet loopback

auto eth0
iface eth0 inet static
  address 10.0.0.100
  netmask 255.255.255.0
  broadcast 10.0.0.255
  gateway 10.0.0.253
EOF

C'est aussi l'occasion de regénérer les clés SSH locales :

rm /etc/ssh/ssh_host_*
dpkg-reconfigure openssh-server

Console et options kernel

Editer dans /etc/defautl/grub :

GRUB_CMDLINE_LINUX_DEFAULT=""  # supprimer "quiet"
GRUB_CMDLINE_LINUX="console=ttyS0,38400n8 console=tty0"

idem, ne pas oublier de reconstruire les binaires de /boot/grub avec : update-grub

Un reboot est probablement indiqué à ce stade, pour faire simple.

Ensuite on crée un répertoire /conf/base dans la racine, et un point de montage pour /cfg

mkdir -p /conf/base
mkdir /cfg

On installe les packages qui vont bien

aptitude install quagga screen iptraf less vlan sudo vim

Séquence de boot

Il faut modifier la séquence d'init pour réaliser les opérations suivantes

/etc/init.d/ramfs

#!/bin/sh
### BEGIN INIT INFO
# Provides:          etc
# Required-Start:    checkfs
# Required-Stop: 
# Default-Start:     S
# Default-Stop:
# Short-Description: Mount all filesystems.
# Description:
### END INIT INFO

mount -t tmpfs -o size=2M varlock /var/lock
mount -t tmpfs -o size=2M varrun /var/run
mount -t tmpfs -o size=16M varlog /var/log
mount -t tmpfs -o size=16M tmp /tmp
cd /var/run; mkdir mdadm network sreen sshd quagga
chown -R quagga: quagga

Il faut aussi modifier mountall pour qu'il dépende de ramfs

sed -e 's/Required-Start:.*/Required-Start: ramfs/' -i mountall.sh

/etc/init.d/etcfs

#!/bin/sh
### BEGIN INIT INFO
# Provides:          etcfs
# Required-Start:    mountall-bootclean
# Required-Stop: 
# Default-Start:     S
# Default-Stop:
# Short-Description: Mount all filesystems.
# Description:
### END INIT INFO

mount -t tmpfs -o size=64M etc /etc
cp -a /conf/base/etc/* /etc/

mount /cfg
cd /cfg
tar cf - * | (cd /etc; tar xf -)

cd
umount /cfg

/etc/init.d/ro-root

#!/bin/sh
### BEGIN INIT INFO
# Provides:          ro-root
# Required-Start:    $all
# Required-Stop: 
# Default-Start:     S
# Default-Stop:
# Short-Description: Mount all filesystems.
# Description:
### END INIT INFO

mount -o remount,ro /

Et ne pas oublier ensuite recontruire les dépendance :

cd /etc/init.d/; insserv *

En finir sur /etc

Editer fstab pour simplifier la suite des opérations de montage/démontage

cat >/etc/fstab <<EOF/dev/sda1       /       ext2    errors=remount-ro       0       1
/dev/sda2       /cfg    ext3    noauto                  0       0
EOF

Enfin, faire une copie de /etc dans /conf/base/etc, ce sera la config « de base » et toutes les différences seront réputées « locales » (donc propres à cette machine en particulier) et donc copiées dans /cfg ensuite.

[ -d /conf/base/etc ] && rm -Rf /conf/base/etc
cd /; cp -a /etc /conf/base/etc

Méchanisme de /cfg

Le script save_cfg

A placer dans /usr/local/sbin/save_cfg :

#!/bin/sh
 
# -a : apply
# -c : cron
# -d : apply
# -g : get conf
# -h : help
# -v : verbose
 
#set -x
 
DEBUG=false
#DEBUG=true
 
APPLY=false
DIFF=false
CRON=false
NEWFILE=""
 
SSHKEY=/etc/ssh/id_dsa_savecfg
TLOGIN="savecfg"
THOST="giw3.gixe.net"
target="-i $SSHKEY $TLOGIN@$THOST"
 
echo_debug ( ) (
        if [ $DEBUG = true ] ;
        then
                echo $1
        fi
)
 
while getopts  "acdghvf" flag
do
 case "$flag" in
        a)
                echo_debug "flag $flag set" ;
                APPLY=true ;
                shift;;
        c)
                echo_debug "flag $flag set" ;
                CRON=true ;
                shift;;
        d)
                echo_debug "flag $flag set" ;
                DIFF=true ;
                shift;;
        f)
                echo_debug "flag $flag set" ;
                shift;
                NEWFILE=$1;
                echo_debug "  with argument $NEWFILE" ;
                shift;;
        g)
                echo_debug "flag $flag set" ;
                trap "/bin/umount /cfg" 1 2 15 EXIT
                /bin/mount /cfg
                (
                        cd /
                        ssh $target /root/make_cfg $2 | tar xf -
                )
                umount /cfg
                trap "" 1 2 15 EXIT
                exit 0 ;;
 
        h)
                echo_debug "flag $flag set" ;
                echo usage : save_cfg [ -adhv | -f <file> ] ;
                echo "    -a : apply - apply the modification" ;
                echo "    -f <file>: add local file to /cfg" ;
                echo "    -c : cron - don't print no save done warning " ;
                echo "    -d : diff  - output the diff between changes" ;
                echo "    -g host.gitoyen.net : get conf, full restore /cfg " ;
                echo "    -h : help  - display this " ;
                echo "    -v : verbose " ;
                exit 0 ;;
 
        v)
                echo_debug "flag $flag set" ;
                #echo flag $flag set ;
                DEBUG=true ;
                shift;;
        *)      echo usage : save_cfg [-adhv]
                exit 0 ;;
 esac
done
 
 
cmp_stat ( ) (
        if [ "$(/usr/bin/stat --format "%a:%B:%Y:%U:%G" $1)" \
           = "$(/usr/bin/stat --format "%a:%B:%Y:%U:%G" $2)" ] ;
        then
                return 0
        else
                return 1
        fi
)
 
cmp_file ( ) (
        if cmp_stat $1 $2 && /usr/bin/cmp -s $1 $2 ;
        then
                return 0
        else
                return 1
        fi
)
 
backup_file ( ) (
        if [ -e $(/usr/bin/dirname /cfg/$1) ] ;
        then
                if [ -f $1 ] ;
                then
                        [ $APPLY = true ] && /bin/cp -pfv /etc/$1 /cfg/$1
                        [ $APPLY = false ] && echo "++  /etc/$1 ->  /cfg/$1"
                else
                        [ $APPLY = true ] && /bin/cp -Rpfv /etc/$1 /cfg/$1
                        [ $APPLY = false ] && echo "+  /etc/$1/ ->  /cfg/$1/"
                fi
        else
                backup_file $(dirname $1)
                [ $APPLY = false ] && echo "+  /etc/$1/ ->  /cfg/$1/"
        fi
)
 
trap "/bin/umount /cfg" 1 2 15 EXIT
/bin/mount /cfg
(
 
#set -x
cd /etc
 
if [ -n "$NEWFILE" ]; then
  NF=${NEWFILE#/etc/}
  if [ -r /etc/$NF ]; then
    echo "** adding the following files to /cfg:"
    tar cvf - $NF | \
      (cd /cfg;
      if [ $APPLY = true ]; then
        tar xf -
      else
        cat >/dev/null
      fi)
  fi
fi
 
for i in `/usr/bin/find * -type f | grep -v mtab`
do
        if [ -f /cfg/$i ]
        then
                echo_debug "File /cfg/$i exist "
                if cmp_file /etc/$i /cfg/$i ;
                then
                        echo_debug "|"
                        echo_debug "--> /etc/$i /cfg/$i identical nothing to do"
                else
                        echo_debug "|"
                        echo_debug "--> Need to backup /etc/$i -> /cfg"
                        [ $DIFF = true ] && /usr/bin/diff /etc/$i /cfg/$i
                        backup_file $i
                fi
        else
                echo_debug "No File /cfg/$i "
                if [ -f /conf/base/etc/$i ] && cmp_file /etc/$i /conf/base/etc/$i ;
                then
                        echo_debug "|"
                        echo_debug "--> /etc/$i /conf/base/etc/$i identical nothing to do"
                else
                        echo_debug "|"
                        echo_debug "--> Need to backup /etc/$i -> /cfg"
                        if [ -f /conf/base/etc/$i ] ;
                        then
                                [ $DIFF = true ] && /usr/bin/diff /etc/$i /conf/base/etc/$i
                        else
                                [ $DIFF = true ] && echo " This is a file creation"
                        fi
                        backup_file $i
                fi
        fi
 
done
 
cd /
#[ $APPLY = true ] && tar cf - cfg | ssh $target /root/commit $(hostname)
)
umount /cfg
trap "" 1 2 15 EXIT
if [ $APPLY = false ] && ! [ $CRON = true ]
then
   echo "**************************"
   echo "* WARNING, NO SAVE DONE  *"
   echo "**************************"
   echo ""
   echo "Launch '/root/save_cfg -a' if you want to apply."
fi

L'essentiel de ce script avait été écrit pour les routeurs de Gitoyen, qui fonctionne un peu différement avec des configs centralisée sous SVN sur un serveur distant, et les routeurs sous nanobsd.

Shell root

Pour ne pas oublier d'invoquer «save_cfg -a» on peut l'appeler dans /root/.bash_logout, ainsi la sauvegarde des modifications locales est faite à chaque fois que root se déloge, tout simplement. Une option utile serait de rendre le script interactif en pareil cas pour pouvoir l'inhiber si besoin (état instable à ne pas sauvegarder par exemple).

#!/bin/sh
/usr/local/sbin/save_cfg -a

Done :-)

A partir de là on a une image utilisable comme routeur.

On sort du chroot, on démonte /mnt et on peut recopier l'image sur une clé USB et tâcher de booter dessus pour voir le résultat.