lid's Wiki

title: Linux: Shell basics
description: Bases shell, iptables, etc
published: true
date: 2023-12-15T15:05:34.459Z
tags: linux, bash, iptables, vim
editor: markdown
dateCreated: 2026-09-24T20:33:13.700Z

Introduction

Une des grandes forces d'UNIX et de GNU/Linux tient dans sa philosophie de "tout est fichier", et plus spécifiquement, tout est fichier et flux de texte. Et UNIX, tout comme GNU/Linux, disposent de nombreux outils (/awk, sed, uniq, sort, etc/) permettant de manipuler ces flux de textes et ces fichiers.

Le /shell/ que nous allons étudier un peu plus en détail s'appelle Bash, pour /Bourne Again SHell/. Il descend du... /Bourne Shell/ et a été développé par Brian Fox pour le projet GNU. Il est apparu dans une première version en 1989, et n'a cessé d'évoluer jusqu'à nos jours (la version 5 date de 2019). C'est le shell par défaut sur la plupart des distributions GNU/Linux, sur macOS jusqu'à macOS Catalina, sur Solaris 11.

Par contre d'autres UNIX utilisent d'autres shells par défaut (même si bash a été porté pour ces OS), par exemple:

Il existe aussi d'autres shells:

Ils sont en général compatibles avec le shell originel, Bourne shell, généralement abrégé /sh/ (1977), descendant du Thompson (comme Ken Thompson) shell, mais diffèrent au niveau de leurs fonctions internes et de leurs possibilités.

ACLs

Droits classiques

Tout d'abord, nous allons (re)voir les droits classiques sous UNIX et GNU/Linux.

#on va d'abord se créer un répertoire
#de bidouille
$ mkdir -p test/{droits,acls,mask}
$ for i in droits acls mask; do for x in a b c; do touch test/$i/$x; done; done

Si on exécute:

$ ls -al
total 12
drwxr-sr-x  3 lidstah lidstah 4096 Sep  7 18:22 .
drwxr-sr-x 42 lidstah lidstah 4096 Sep  7 18:20 ..
-rw-r--r--  1 lidstah lidstah    0 Sep  7 18:20 bla
drwxr-sr-x  2 lidstah lidstah 4096 Sep  7 18:22 lol
-rw-r--r--  1 lidstah lidstah    0 Sep  7 18:20 plop
-rw-r--r--  1 lidstah lidstah    0 Sep  7 18:20 pwet

On remarque un premier champ composé de commutateurs. Par exemple:

drwxr-sr-x  2 lidstah lidstah 4096 Sep  7 18:22 lol

M'indique que le fichier est un répertoire (/d/), qui est lisible, modifiable et exécutable (rwx) par mon utilisateur, lisible et exécutable (le s signifie /setgid/ et implique ici le x d'exécutable) par le groupe "lidstah" et enfin lisible et exécutable par tout le monde.

NOTE: un répertoire est TOUJOURS exécutable

NOTE bis: on verra setuid, setgid et sticky bit un peu plus loin dans ce chapitre 😃

Ce qui nous amène aux groupes de permissions et types de permissions!

Groupes de permissions

Les permissions sont organisées en trois groupes:

Petit récapitulatif pour la notation /chmod/:

char signification
u utilisateur
g groupe
o les Autres
a Tous

Types de permissions

Là aussi, on a trois types de permissions (on appelle une /triade/ ces trois types de permissions)

Quels utilitaires nous permettent de modifier les permissions d'un fichier?

On a deux grands copains pour faire ça:

#Changer l'owner et le groupe d'un fichier:
$ chown user:group fichier
#Changer de façon récursive l'utilisateur et le groupe 
#d'un répertoire et de son contenu:
$ chown -R user:group repertoire
#Changer, pour l'utilisateur courant, les droits du 
#fichier en read, write, execute
$ chmod u+rwx fichier
#Changer, pour l'utilisateur courant, les droits du 
#fichier en rwx, et enlever tous les droits au 
#groupe et aux Autres, deux méthodes:
$ chmod u+rwx,g-rwx,o-rwx fichier
#Ou plus simplement:
$ chmod 700 fichier
#On peut aussi directement affecter les droits avec a
$ chmod a+rw fichier

En effet, chmod permet d'utiliser une notation "humaine" et une notation "octale" pour la définition des droits.

Un petit tableau sur les valeurs octales et leur signification:

Valeur Droits
4 Read
2 Write
1 Execute
0 None

Et donc, pour les droits rwx: 4+2+1 = 7, rw: 4+2 = 6, read-only (read): 4, read et execute: 4+1 = 5... etc. On va pouvoir rapidement mettre ça en pratique 😃

Je vous incite fortement à lire les /manpages/ correspondantes 😃

Et maintenant, quelques exercices (simples). On va tout d'abord créer un répertoire "test" et quelques fichiers à l'intérieur de ce répertoire:

$ mkdir test && for i in a b c; do touch test/$i; done

#Ce qui va nous donner quelque chose du genre:
$ ls -al test
total 8
drwxr-sr-x  2 lidstah lidstah 4096 Sep  7 18:43 .
drwxr-sr-x 42 lidstah lidstah 4096 Sep  7 18:43 ..
-rw-r--r--  1 lidstah lidstah    0 Sep  7 18:43 a
-rw-r--r--  1 lidstah lidstah    0 Sep  7 18:43 b
-rw-r--r--  1 lidstah lidstah    0 Sep  7 18:43 c

Exercices

Quelles commandes me permettraient:

  1. De modifier les droits de tous les fichiers du répertoire pour qu'ils soient lisibles, modifiables et exécutables par tout le monde? NOTE: NE JAMAIS, JAMAIS, faire ça 😃
  2. C'est pour ça qu'on va maintenant les modifier pour qu'ils soient lisibles, modifiables uniquement par mon utilisateur
  3. Il faudrait maintenant que b soit aussi lisible pour tout le monde et modifiable par l'utilisateur et son groupe, comment faire?
  4. Que se passe-t-il si je chmod 400 le fichier c? puis-je le supprimer? remodifier ses droits?

Droits Spéciaux

suid

SUID signifie /Set User ID/ bit. Il permet de lancer un programme avec les droits de son propriétaire.

Vous l'utilisez très souvent sans vous en rendre compte. Si on regarde:

$ ls -al /usr/bin/sudo
-rwsr-xr-x 1 root root 136976 May 25 09:21 /usr/bin/sudo

Ici, on voit que l'exécutable sudo (/Summon User DO/) appartient à root et au groupe root, et que le droit d'exécution de l'utilisateur a été remplacé par /setuid/.

De même, passwd:

$ ls -al /bin/passwd
-rwsr-xr-x 1 root root 63736 juil. 27  2018 /bin/passwd

Ici, quel est l'intérêt que passwd soit /setuid/? Tout simplement car passwd modifie le contenu du fichier:

/etc/shadow

lequel appartient à root:root. Hors, un utilisateur peut vouloir changer son mot de passe. Il faut donc que lorsqu'il invoque le programme passwd, ce programme, lancé par cet utilisateur, puisse modifier le fichier shadow appartenant à root:root.

Si on lance la commande passwd en tant qu'utilisateur et qu'on regarde la liste des processus, on va voir que passwd est lancé en tant que root:

#dans un premier shell, on tape la commande passwd
$ passwd
Changement du mot de passe pour lidstah.
Current password:
#dans un second shell
#(tmux, autre console virtuelle (ctrl-alt-Fx)
$ ps aux|grep passwd
root     15038  0.0  0.0   9356  2880 pts/2    S+   21:30   0:00 passwd
lidstah  15047  0.0  0.0   6092   892 pts/4    S+   21:30   0:00 grep passwd

Dès lors, on voit que le bit SUID permet de lancer des programmes et commandes avec les droits du propriétaire du fichier, plutôt que ceux de l'utilisateur qui lance le programme. Dans le cas de passwd, on ne veut pas qu'un utilisateur puisse modifier le mot de passe d'un autre utilisateur. Cette logique est intégrée dans le logiciel passwd, pas dans le mécanisme de SUID ou dans la logique du kernel.

Il est donc très important, lorsqu'on crée un programme ou un script exécutable avec le bit SUID, de considérer dès le début l'aspect sécurité et de ne jamais le négliger: "Security First".

On peut activer le bit SUID sur un exécutable de la sorte:

$ chmod u+s fichier

sgid

SGID, je vous le donne dans le mille, signifie "Set Group ID". Il est très similaire à SUID, sauf qu'il permet d'exécuter un processus avec les droits du groupe plutôt que les droits de l'utilisateur propriétaire du fichier.

On peut l'activer via la commande:

$ chown user:group fichier
$ chmod u-s,g+s fichier

Une autre utilité est de pouvoir créer un répertoire appartenant à un groupe, le SGID, et ainsi tous les fichiers créés dans ce répertoire appartiendront au groupe SGID:

$ mkdir sgidtest
$ chown user:groupe sgidtest
$ chmod g+s sgidtest
$ touch sgidtest/plop
$ ls -al sgidtest

sticky bit

Le /sticky bit/ est un droit d'accès qui peut être principalement assigné aux répertoires. Son apparition remonte à 4.3BSD en 1986, ce qui ne me rajeunit pas 😃.

Typiquement, il permet de limiter les droits de suppression et renommage du répertoire et de son contenu uniquement à root et à l'utilisateur ayant créé le répertoire. Par exemple, si le groupe dispose des autorisations de lecture et écriture, un utilisateur membre de ce groupe pourra modifier le fichier et le lire, mais ne pourra pas le renommer, le déplacer ou le supprimer. Seul le propriétaire du fichier et root disposeront de ces droits. On le rencontre souvent dans /tmp:

$ ls -al /tmp
total 48
drwxrwxrwt 10 root    root    4096 Sep  7 18:52 .
drwxr-xr-x 22 root    root    4096 Apr 17 17:05 ..
drwxrwxrwt  2 root    root    4096 Sep  7 17:57 .ICE-unix
-r--r--r--  1 root    lidstah   11 Sep  7 17:58 .X0-lock
drwxrwxrwt  2 root    root    4096 Sep  7 17:58 .X11-unix
drwx------  2 lidstah lidstah 4096 Sep  7 18:01 Temp-2f0c98b9-3af5-4e0c-a19f-c6819188f606
drwx------  2 lidstah lidstah 4096 Sep  7 18:01 Temp-80daa3df-3a93-424e-8d2f-3468c4147f89
drwx------  2 lidstah lidstah 4096 Sep  7 17:58 babel-vn4wSf
drwx------  2 lidstah lidstah 4096 Sep  7 17:58 emacs1000
drwx------  2 lidstah lidstah 4096 Sep  7 17:58 ns.lidstah.:0
-rw-------  1 lidstah lidstah  100 Sep  7 17:58 serverauth.LRiGaenhh6
srw-------  1 lidstah lidstah    0 Sep  7 18:01 ssh_mux_192.168.1.227_10022_git
srw-------  1 lidstah lidstah    0 Sep  7 17:58 ssh_mux_89.234.176.135_22_lidstah
srw-------  1 lidstah lidstah    0 Sep  7 18:25 ssh_mux_docker-prod.lidstah.info_22_lidstah
srw-------  1 lidstah lidstah    0 Sep  7 17:58 ssh_mux_providence.lidstah.info_22_lidstah
drwx------  2 lidstah lidstah 4096 Sep  7 17:58 tmux-1000

Ici, par exemple, on voit bien que les répertoires .ICE-unix et .X11-unix ont le sticky bit activé (le "t" à la place du "x" du groupe de permissions "les Autres"). L'activation du sticky bit "t" implique que les droits d'exécution sont aussi implicitement activés. Plus rare, on peut rencontrer le sticky bit "T", qui n'active pas les droits d'exécution - et est donc inutile sur un répertoire. Le noyau Linux ignore le sticky bit sur les fichiers - ce qui n'est pas le cas de tous les UNIX.

Comment manipuler le sticky bit

C'est très simple, avec notre ami /chmod/ bien sûr!

#On peut le set en utilisant le commutateur +t
$ chmod +t test/
#Ou en utilisant sa dénomination octale, 1000:
$ chmod 1755 test/

#De même on peut le supprimer:
$ chmod -t test/
$ chmod 0755 test/

ACL - Installation

Tout d'abord, il faut vérifier si notre kernel supporte les /Access Control Lists/, par exemple ici sur une Alpine Linux 3.12:

$ grep ACL /boot/config-*
CONFIG_EXT2_FS_POSIX_ACL=y
CONFIG_EXT3_FS_POSIX_ACL=y
CONFIG_EXT4_FS_POSIX_ACL=y
CONFIG_REISERFS_FS_POSIX_ACL=y
CONFIG_JFS_POSIX_ACL=y
CONFIG_XFS_POSIX_ACL=y
CONFIG_BTRFS_FS_POSIX_ACL=y
CONFIG_F2FS_FS_POSIX_ACL=y
CONFIG_FS_POSIX_ACL=y
CONFIG_TMPFS_POSIX_ACL=y
CONFIG_JFFS2_FS_POSIX_ACL=y
# CONFIG_NFS_V3_ACL is not set
# CONFIG_NFSD_V3_ACL is not set
CONFIG_CEPH_FS_POSIX_ACL=y
CONFIG_9P_FS_POSIX_ACL=y

La ligne qui nous intéresse principalement est:

CONFIG_FS_POSIX_ACL=y

On peut avoir besoin de permissions plus fines que ce qu'on peut obtenir via les permissions de base du système de fichiers ou de SUID/SGID. Imaginons que vous vouliez partager un fichier dans un répertoire de votre $HOME à un utilisateur n'appartenant pas à votre groupe. Il faut trouver un moyen de l'autoriser à accéder à votre fichier, pour pouvoir le lire ou le modifier par exemple.

Pour ça, on peut utiliser /acl/.

Il s'installe très simplement sous Debian:

$ sudo apt install acl

Et voilà!

ACL - Les commandes

getfacl

La commande getfacl permet d'obtenir des informations sur les ACLs attribuées à un fichier ou répertoire, et si elles s'appliquent - ou pas 😃 (notamment si on /chmod/ un fichier après l'avoir /setfacl/ 😃)

setfacl

/setfacl/ est la commande nous permettant d'ajouter, supprimer et modifier une ACL sur un fichier, par exemple:

$ touch test/acltest
$ setfacl -m u:bernard:rw- test/acltest

ici, l'utilisateur "bernard" (ahem) disposera désormais des droits en lecture et écriture sur le fichier test/acltest.

La syntaxe de /setfacl/ est toujours de la forme:

prefix:utilisateurOugroupe:droits

Les préfixes disponibles sont:

char signification
u droits utilisateur
g droits groupe
o droits Autres

NOTE: pour le préfixe "o" on ne précise évidemment pas d'utilisateur ou de groupe, ce qui est logique puisqu'on parle de droits assignés aux Autres. 😃

Et bien sûr les droits habituels: r, w et x.

On peut aussi vouloir appliquer récursivement une ACL sur tout un répertoire:

$ setfacl -Rm u:bernard:rw- test/

Et on peut bien sûr préciser une liste d'utilisateurs et groupes:

$ setfacl -m u:bernard:rw-,u:raymonde:rwx,g:pmu:r--,o:--- test/acltest

Maintenant, comment vérifier qu'un fichier ou répertoire est visé par une ou plusieurs ACL?

Tout d'abord, on peut lister le(s) fichier(s) en question:

$ ls -al test/acltest 
-rw-rw-r--+ 1 lidstah lidstah 0 sept.  8 18:46 test/acltest

Ici le "+" en fin de triades nous indique que des acls s'appliquent sur ce fichier.

Et donc, avec getfacl (ici c'est l'utilisateur www-data qui dispose de droits sur ce fichier):

$ getfacl test/acltest 
# file: test/acltest
# owner: lidstah
# group: lidstah
user::rw-
user:www-data:rw-
group::r--
mask::rw-
other::r--

Pour supprimer une ACL, on utilisera:

$ setfacl -b fichier

On peut aussi supprimer une partie de l'ACL avec l'option -x:

$ setfacl -x u:bernard,g:pmu test/acltest

Droits par défaut et héritage

On peut définir des droits par défaut sur le contenu d'un répertoire avec le... pré-préfixe d:

$ setfacl -m d:u:www-data:rwx test
#ou, plus simple si on a plusieurs users/groupes:
$ setfacl -dm u:www-data:rw-,g:pmu:r--,o:--- test/

Ainsi, tous les fichiers créés dans le répertoire test pourront être lus, écris et modifiés par www-data. On peut légitimement se poser la question de l'intérêt de ce genre de droits, mais bon 😃

Gestion du masque

Sous GNU/Linux et UNIX, les nouveaux fichiers sont créés avec un ensemble de permissions par défaut. Enfin, en vrai, les permissions d'un nouveau fichier sont restreintes par un masque qui est appelé /umask/

On utilisera donc l'utilitaire /umask(2)/ - la manpage est rigolote - pour vérifier sa valeur, ou modifier ce masque.

# afficher le masque actuel
$ umask -S
u=rwx,g=rx,o=rx
# et en octal
$ umask
0022

Maintenant, on peut modifier ce masque (notamment dans des scripts shell 😃), par exemple, pour que les fichiers que nous (ou notre script) vont créer soit uniquement lisibles par tout le monde:

$ umask u=r,g=r,o=r
# ou plus court:
$ umask a=r

Si on veut rigoler un peu, on peut faire en sorte que par défaut, tous les fichiers créés n'aient aucune permissions:

$ umask a=

Ici, le "=" prend la place de toutes les permissions.

On peut aussi décider de créer nos fichiers pour qu'ils soient, par défaut, exécutables par notre groupe, sans changer les autres droits:

$ umask g+x

Bien évidemment, on peut aussi utiliser la représentation octale pour modifier le /umask/.

Maintenant, sous UNIX et GNU/Linux, les permissions "de base" pour un fichier sont 666 et pour un répertoire 777. C'est le masque /umask/ qui permet de limiter ces droits à la création.

Par exemple:

$ touch masque #no joke
$ ls -al masque
-rw-r--r-- 1 lidstah lidstah 0 sept.  9 19:27 masque

Ici, on voit que les permissions, une fois le fichier créé et le masque appliqué, sont 644, soit, 666 - 022.

De même, si je crée un répertoire, ses permissions par défaut de 777 deviendront 755 une fois le masque 022 appliqué (777 - 022).

On peut donc se faire un petit tableau récapitulatif des valeurs de masque et des droits par défaut qu'il confère sur les fichiers et les répertoires:

valeur bit umask permissions fichier permissions répertoire
0 rw rwx
1 rw rw
2 r rx
3 r r
4 w wx
5 w w
6 x x
7 nada que dalle

Expressions Régulières ("Regex")

Une expression régulière, a.k.a. /regex/, "Regular Expression", qu'on appelle aussi expressions rationnelles, c'est une séquence de caractères qui définie un modèle de recherche (typiquement, dans du texte).

Ce concept a été créé dans les années 50, notamment par Stephen Cole Kleene qui en a formalisé la description. Il a été popularisé par UNIX et notamment ses utilitaires de manipulation de texte (/grep/, /awk/, /sed/, /ed/, etc). Ken Thompson (un des créateurs d'UNIX avec Dennis Ritchie) avait implémenté dans les années 60 un éditeur (QED) implémentant les /regexes/ de Kleene sur CTS, et en a profité pour inventer la compilation JIT (/Just In Time/) dans la foulée.

On catégorise les expressions régulières en deux grands groupes:

On les utilise énormément dans les traitements de texte (sous le capot), éditeurs de texte (c'est vous qui la tapez :p), dans l'analyse lexicale (compilateurs), et donc dans à peu près tous les langages de programmation modernes, dans les utilitaires de manipulation de texte (précités: grep, awk, etc), dans les fichiers de configuration de serveurs Web (nginx, apache2, etc), de reverses proxies (HAProxy, nginx, etc), de cache web (Varnish par exemple, nuster, etc) afin de lier des actions ou ACLs à des /patterns/.

On peut les utiliser pour rechercher rapidement toutes les occurences d'un /pattern/ dans du code, du texte, une base de données (par exemple PostGreSQL qui utilise les /regexes/ de Spencer (tcl)), etc...

Par exemple, on peut utiliser des regexes pour mettre en cache (ou non) du contenu en fonction de l'hôte, de l'user-agent, de l'URL visitée, ou pour envoyer les bots de bing, Baidu et Yandex vers une null-backend leur envoyant une 429 "Too Many Requests" (même si j'aimerai bien qu'on invente le code de retour "430 f*** you"), et tous les autres bots vers une backend de /rate-limit/.

On peut les utiliser pour /parser/ des logs, en extraire les données pertinentes et les envoyer vers un agrégateur de logs (rsyslog, logstash) et effectuer de plus amples traitements (à coups de regexes là encore) afin d'en extraire des métriques métier, business, etc, En vrai, on les utilise tout le temps 😃

/Note: à propos de Grep et Awk, je ne saurais que trop vous recommander les vidéos de la chaîne Youtube Computerphile avec Brian Kernighan, qui a écrit AWK avec Alfred Aho et Peter Weinberger - A,W,K - et qui a aussi écrit, avec Dennis Ritchie, C Programming Language, la référence - en un mot comme en cent, ce type est un des pères fondateurs des sciences de l'informatique./

Métacaractères et regexes POSIX

Tableau récapitulatif

Tout d'abord, un petit tableau qui va nous permettre d'énumérer les métacaractères qui sont un des fondements du "langage" des expressions régulières:

Symbole Définition
^ Début de ligne
$ fin de ligne
< "curseur" en début de mot
> "curseur" en fin de mot
. n'importe quel caractère
... tout caractère énuméré entre les crochets
[^...] tout sauf les caractères énumérés
? une occurence ou aucune du caractère ou de la regex précédente
* 0 ou x occurence(s) du caractère ou de la regex précédente
pipe séparateur OR
\b mot
\B pas un mot
\w tout caractère alphanumérique (et underscore)
\W tout caractère non-alphanumérique
\d tout chiffre
\D n'est pas un chiffre
\s whitespace (espace)
\S n'est pas un espace
A-Z Toutes les majuscules de A à Z (combinable)
0-9A-Z Toutes les majuscules et tous les chiffres
/Note: le caractère pipe " " ne passe pas dans les tableaux org-mode 😉/

/Note bis: notez la différence de signification du métacaractère "*" avec le caractère joker du shell!/

Exemples simples (inutiles, ou pas 😃)

$ egrep '^[A-Z]' fichier

$ egrep '^[a-z]' fichier

$ egrep '[0-9]' fichier

$ egrep '^[a-z].*[0-9]' fichier

$ sed 's/^[AD].*$/pwet/g' test

/\bU\w+
# la regex étant \bU\w+

Ici, le commutateur \b indique de sélectionner tous les mots commençant par U, en /matchant/ tous les caractères alphanumériques (+ underscore) (\w) jusqu'au prochain caractère n'étant pas /matché/ par \w (+)

Globs bash

Bash ne supporte pas les regexes POSIX, contrairement à beaucoup de langages de programmation et d'utilitaires du système (grep, sed, awk, vim, etc) ou logiciels (nginx, apache, haproxy, etc). À la place il utilise un mécanisme qu'on appelle /globbing/ afin de lui permettre de proposer des fonctionnalités équivalentes.

Tout d'abord, tout comme pour les regexes, un petit tableau récapitulatif:

caractère signification exemple
? /matche/ n'importe quel caractère tout seul ls ??.c
* /matche/ zéro ou plusieurs caractère(s) ls b*.sh
A-Z /matche/ le caractère s'il est dans l'intervalle ls a-z??.sh
^ début de ligne grep '^[a-d]' fichier
! idem que ^ mais dans le range grep '!a-d' fichier
$ fin de ligne
{pat1,pat2,...} /matche/ les noms de fichiers correspondant aux patterns rm {*.doc,???.xlsx}
| applique une ou plusieurs conditions au glob pattern exemple plus bas 😉
$ ls a*+(.c|.sh|.ksh)

#!/bin/bash

echo "Quelle opération voulez vous faire?"
read reponse
case $reponse in
  1 | R ) echo "Reboot";;
  2 | P ) echo "Poweroff";;
  *) echo "nope nope nope";;
esac

Vim (Vi iMproved)

Historique

Au début il y avait /ex/ - qui était lui même une extension de l'éditeur de texte /ed/. Bill Joy, en 1976, créa une extension à /ex/ qu'il nomma /vi/, pour /visual/. En effet, l'édition, dans /ed/ et /ex/, se faisait ligne par ligne. /vi/ permettait d'utiliser /ex/ en mode plein-écran, et donc d'avoir suivant le terminal utilisé de 25 à 40 lignes de texte sur 80 colonnes utilisables, ce qui était une nette progression par rapport à l'édition ligne par ligne.

Il est à noter que Bill Joy est un des principaux contributeur des premières version de BSD UNIX (/Berkeley Software Distribution/), et aussi un des membres fondateurs de Sun Microsystems. Il a aussi créé un /shell/, le C-shell (csh, et son successeur tcsh), dont la syntaxe se rapproche plus du langage C que la syntaxe de /Bash/.

À la fin des années 80, le programmeur Bram Moolenaar (RIP 2023, à 62 ans 😦 ), heureux acquéreur d'un Amiga (une machine et un OS géniaux à l'époque, en avance sur leur temps d'une bonne décennie), était insatisfait des éditeurs de texte disponibles sur cette machine. En effet, il était habitué à travailler sur des stations UNIX où l'éditeur consacré était /vi/. Il a donc développé en 1988, sur la base de l'éditeur /STvi/, /Stevie/ originaire de l'Atari ST, un clone de vi, qu'il a nommé VIM, pour /Vi IMitation/, dont l'objectif était au départ de recréer les fonctionnalités de /vi./ Au fur et à mesure de son évolution, de nombreuses fonctionnalités ont été ajoutées (tabs, splits, plugins, fonctions d'éditions avancées, VimScript, correction orthographique, folding, etc) et son nom a évolué en /Vi IMproved/.

Il est possible de le transformer en IDE complet via ses nombreux plugins (plugins Git, plugins de CI/CD, etc), même si ses fonctionnalités de base sont en général suffisantes pour la plupart des utilisations.

Pour la petite histoire, ça fait maintenant 31 ans que j'utilise Vim - je l'ai utilisé pour la première fois en 1993 sur Amiga, grâce aux disquettes de logiciels libres /Fred Fish/ (RIP 😦). Même si j'utilise désormais principalement Emacs sur mes postes de travail (avec le plugin Evil permettant de retrouver les modes, raccourcis et fonctionnalités de VIM dans Emacs), je m'en sers quasiment tous les jours dans mon métier: /vi/ est quasiment toujours disponible dans les utilitaires de base d'un UNIX ou UNIX-like, et Vim est en général toujours disponible dans les paquets de ces distributions.

Installation

Très simple, suivant votre distribution, ici Debian-like:

$ sudo apt install vim

Configuration

La configuration de Vim s'effectue dans deux fichiers:

# pour tout le système
/etc/vim/vimrc
# pour l'utilisateur
~/.vimrc

On va donc lancer Vim en lui disant de modifier directement:

$ vim .vimrc

Vous devriez obtenir quelque chose de ce style:

img

C'est assez dépouillé, non? On va y revenir plus tard, mais vous démarrez en mode /Normal/. C'est le mode qui permet de manipuler des blocs (mots, paragraphes, caractères, lignes, etc) de texte. Mais pas d'insérer du texte. Pour se faire, on va passer en mode d'insertion en appuyant sur i, ce qui va nous donner ça:

img

Remarquez la dernière ligne qui indique désormais -- INSERTION -- afin de vous signifier que vous êtes en mode d'insertion.

Par défaut, Vim n'active ni la numérotation des lignes, ni la coloration syntaxique. On va remédier à ce problème, en tapant:

set nu
syntax on

Maintenant se posent deux questions:

  1. Comment sortir du mode d'insertion?
  2. Comment sauvegarder notre fichier et quitter Vim?

Quel que soit le mode (hormis le mode Normal) on peut toujours revenir au mode normal en appuyant sur Esc (et en général, pour faire bonne mesure, j'appuie toujours deux fois de suite sur Esc). Un autre moyen est la combinaison de touches Ctrl-c.

Normalement, vous devriez tous être revenus au mode normal.

Si vous tapez ":" vous allez passer en mode ligne de commande, et vous allez pouvoir taper une commande vim pour sauvegarder (w) et quitter (q). Vous devriez avoir quelque chose comme ça:

img

Il suffit d'appuyer sur Entrée pour valider et donc sauvegarder et quitter Vim.

On aurait pû aussi tout simplement, en revenant en mode normal, taper directement ZZ pour sauvegarder et quitter sans avoir à taper :wq. De même, on aurait aussi pu juste taper :x au lieu de :wq, x étant ici un synonyme de wq en mode ligne de commande.

De même, on pourrait vouloir quitter sans sauvegarder, dans ce cas, toujours en mode normal, on taperait:

:q!

Voilà, vous venez d'effectuer vos tous premiers pas sur Vim, c'est pas si compliqué, si? Maintenant, on va voir comment l'utiliser au quotidien. Alors, bien évidemment, il y a beaucoup, beaucoup, beaucoup d'options de configurations dans Vim, mais l'objectif de ce cours est de vous permettre de l'utiliser correctement à un niveau débutant. Au bout de 27 ans, il m'arrive encore assez souvent de découvrir de nouveaux "trucs" possibles avec Vim. C'est un éditeur dont la courbe d'apprentissage est très élevée, mais dont la maîtrise vaut vraiment le coup.

Utilisation (de base)

Un éditeur de texte modal

Ainsi qu'on vient juste de le voir, Vim est un éditeur /modal/. Nous avons vu très rapidement le mode Normal, le mode d'Insertion, le mode ligne de commande, et comment revenir au mode Normal (Esc ou Ctrl-c).

Mais quels sont tous les modes de Vim?

Mode Touches Fonctionnalités
Normal Esc,C-c manipulations de texte (suppression, copier/coller, déplacement rapide)
Insertion i,I,A,O,r,R,s,S Insertion,remplacement, etc de texte 😃
ligne de commande : commandes Vim (sauvegarder, quitter, exécuter une commande shell, etc)
Visuel v, S-v, C-v Sélection avancée (caractère, ligne, bloc) pour manipulation de texte
Sélection Visuel -> sélectionner -> Ctrl-G -> Insertion
Ex Q Q -> ex-mode -> ensemble de commandes Vim -> :vi pour repasser en normal

Buffers, tabs, splits

Buffers

Tout d'abord, il faut savoir que tout fichier ouvert dans Vim est copié dans un /buffer/ (tampon), où les modifications seront effectuée, ce /buffer/ étant écrit sur le fichier lorsqu'on exécute la commande :w. Bien évidemment, Vim permet de travailler sur plusieurs buffers en même temps.

Plutôt qu'une longue explication, touffue, on va plutôt regarder directement comment ça fonctionne.

On va d'abord lancer Vim:

$ vim

On a aucun fichier ouvert, on va en ouvrir un (en read-only). Pour ça, en mode Normal, on va taper :e chemin/nom_du_fichier, par exemple:

:e /etc/fstab

Puis on va en ouvrir un second:

:e /etc/bash.bashrc

Zut, notre premier fichier a disparu! Rassurez-vous, il est toujours là! si on tape la commande :buffers on va obtenir quelque chose de ce style:

img

Là, on voit que j'ai deux buffers ouverts:

Le numéro 1, fstab, et le second (qui est celui sélectionné, d'où le "a"ctive) qui est mon fichier bash.bashrc. On appuie sur entrée pour sortir du mode buffer. Maintenant, si je veux travailler sur mon fstab, c'est à dire mon buffer numéro 1, je vais simplement taper:

:b1

Et ainsi de suite...

Tabs

Vim nous permet aussi d'utiliser des onglets, ce qui peut être pratique si on édite quelques fichiers et qu'on veut naviguer rapidement entre eux.

Ouvrons un nouvel onglet:

:tabnew

Maintenant, ce nouvel onglet est vide. Je vais donc lui assigner un de mes buffers, par exemple notre buffer numéro 2, bash.bashrc, qui n'est pas visible actuellement. Je tape donc :b2, et je devrais obtenir quelque chose de ce style:

img

Pour bouger d'un onglet à l'autre, en mode Normal, je vais taper gt pour passer à l'onglet suivant, et gT pour passer à l'onglet précédent.

Splits

On a vu les buffers, les onglets, c'est bien sympa, mais parfois on peut travailler sur des fichiers de plusieurs centaines voire milliers de lignes, et il peut être utile d'avoir des divisions verticales ou horizontales et dans chacun de ses splits une vue différente du fichier qu'on édite, ou plus simplement des fichiers différents dans chaque /split/. Par exemple, ça donne ça avec un fichier de config HAProxy:

img

Reprenons notre fichier bash.bashrc, dans notre second onglet. On va créer un split vertical. On se place en mode Normal, puis on tape: Ctrl-w v (c'est à dire: Ctrl-w puis v). Et hop. Pour passer d'un split à l'autre, on peut utiliser le raccourci Ctrl-w w (ou Ctrl-w h pour aller à gauche, Ctrl-w l pour aller à droite, Ctrl-w j pour aller en bas, et Ctrl-w k pour aller en haut).

Si on veut créer un split horizontal, on utilisera Ctrl-w n. Pour fermer un split, on fait simplement :q.

Les splits sont aussi pratiques pour avoir, par exemple, la /manpage/ correspondante au fichier de configuration qu'on est en train de modifier, ou de la doc, etc.

Manipulations de base, déplacement, copier/coller (yank/paste), sélections visuelles

Notion de verbe, complément, objet

Les commandes de Vim répondent à une syntaxe précise. par exemple di" signifie "Delete Into double-quotes", et provoque la suppression des caractères situés entre deux double-quotes. De la même manière, ciw signifie Change In Word, et provoquera la suppression du mot sur lequel le curseur se trouve et le passage en mode d'insertion. De la même façon, d$ signifie "supprime tout du curseur à la fin de la ligne".

Dès lors, une commande comme:

ggdG

Signifiera: déplace toi au début du fichier et supprime le contenu jusqu'à la fin du fichier. (bourrin)

ci) ou ci} effectueront la même chose entre deux parenthèses, etc.

de même, yi" signifie "Yank in quotes" et copiera le contenu situé entre deux double-quotes.

Déplacements

Alors, lorsqu'on est en mode normal, on se déplace non pas avec les touches du curseur, mais avec les touches h, j, k et l. Pourquoi? Alors déjà car c'est mieux d'un point de vue ergonomie d'avoir toujours les mains sur le /home row/ de son clavier, et ensuite... un peu à cause de ça:

img

Ça c'est le clavier d'un vieux terminal des années 70. Notez les touches H,J,K et L de ce clavier 😃

Alors bien sûr, se déplacer caractère par caractère et ligne par ligne dans un fichier de plusieurs centaines de lignes peut vite devenir... lourdingue 😃. Pour ça, on a plusieurs possibilités (toujours en mode normal, hein):

Combo Effet
H déplace à la première ligne visible
L déplace à la dernière ligne visible
M déplace au milieu de l'aire visible
gg déplace au début du fichier
G déplace à la fin du fichier
160G déplace à la ligne 160
fz déplace à la première occurence de z dans la ligne depuis le curseur
Fz déplace à la premiere occurence de z dans la ligne AVANT le curseur
t,T idem que f et F mais déplace au caractère précédent le caractère recherché
0 déplace au début de ligne
^ déplace au premier caractère de la ligne
$ déplace à la fin de la ligne
A déplace après le dernier caractère de la ligne et passe en mode d'insertion
I déplace au début de la ligne et passe en mode d'insertion
/ lance la recherche sur un pattern
n,N navigue entre les résultats de la dernière recherche
mX crée une marque X au curseur
'X revient sur la marque X (marques possibles: a-zA-Z
w déplace de mot en mot
b déplace de mot en mot en arrière
E déplace de mot en mot en positionnant à la fin de chaque mot
Ctrl-d, Ctrl-u déplace d'une demi page en avant ou en arrière
déplace de paragraphe en paragraphe
% déplace du début à la fin d'un bloc de code et inversement

Exercez vous sur le buffer bash.bashrc que nous avons ouvert pour vous faire la main. Je ne vous demande pas d'apprendre tout ça par coeur hein, mais avec le temps vous verrez que ça devient comme une seconde nature 😃

Notez aussi que la majorité des commandes de Vim permettent, en les préfixant d'un nombre, de les exécuter plusieurs fois. Par exemple 4w vous déplacera de 4 mots.

Suppression, remplacer du texte, undo, redo, copier, coller

Pour supprimer une ligne directement, on peut taper dd. La ligne est alors placée dans le presse-papiers. on peut alors la coller après le curseur avec p et avant le curseur avec P.

De même, on peut copier (/yank/) une ligne directement en tapant yy.

On peut, de même, supprimer ou copier du texte en suffixant l'opérateur d ou y d'un opérateur de mouvement.

Par exemple:

# supprimer 2 mots
d2w
# yanker 2 mots
y2w
# supprimer jusqu'au caractère X
dtX
# copier jusqu'au caractère X
ytX

On peut remplacer un caractère en se positionnant dessus, puis en tapant r et le caractère voulu.

On peut passer en "mode" de remplacement en tapant R puis le texte remplaçant l'ancien texte. Comme pour le mode d'insertion, il suffit de taper Esc ou Ctrl-c pour sortir de ce mode.

On peut aussi supprimer le caractère sous le curseur avec la touche x.

Exemple: imaginons que j'ai fait une typo, j'ai tapé outli au lieu d'outil:

# je me place sur le l
outli
# je tape xp
outil

Enfin, Vim propose un undo infini. En mode normal, on utilise u pour undo, et Ctrl-r pour redo.

On peut citer des plugins sympathiques comme Gundo pour obtenir un undo plus puissant (arbre des éditions, etc).

Dernière note, pour répéter la dernière commande - par exemple de mouvement, on peut utiliser la touche ;.

Sélection visuelles

Bien sûr, on peut aussi utiliser les modes visuel, visuel-ligne et visuel-bloc pour respectivement sélectionner depuis le curseur, sélectionner des lignes, ou sélectionner des blocs et leur appliquer les mêmes manipulations qu'en mode normal.

Combo Mode
v visuel
Shift-v visuel ligne
Ctrl-v visuel bloc

On effectue notre sélection de la même façon qu'on se déplace (hjkl, 160G (pour sélectionner depuis le curseur jusqu'à la ligne 160, etc), et ainsi de suite). Puis on sélectionne l'action à effectuer.

Là je vais utiliser des exemples visuels que je vous recommande chaudement d'effectuer vous aussi sur votre poste de travail, mais imaginons ce bloc de configuration haproxy:

server worker1 172.17.0.1:8080 check
server worker2 172.17.0.2:8080 check
server worker3 172.17.0.3:8080 check
server worker4 172.17.0.4:8080 check

Disons que je veux ajouter rapidement les options maxconn 10 et maxqueue 20 afin de limiter le nombre de connexions concurrentes sur chaque serveur (disons que c'est une backend de garage pour bots de crawl). Je vais d'abord me placer à la fin de la première ligne, puis activer le mode visuel-bloc, "descendre" jusqu'à la dernière ligne, puis taper (toujours en mode visuel bloc):

#A pour insérer après le dernier caractère de la ligne
Amaxconn 10 maxqueue 20 Esc

Ce qui va me donner:

server worker1 172.17.0.1:8080 check maxconn 10 maxqueue 20 
server worker2 172.17.0.2:8080 check maxconn 10 maxqueue 20 
server worker3 172.17.0.3:8080 check maxconn 10 maxqueue 20 
server worker4 172.17.0.4:8080 check maxconn 10 maxqueue 20 

Comme vous voyez, c'est quand même pratique, non? 😃

Macros

Bien évidemment, on peut utiliser des macros avec Vim.

Pour activer l'enregistrement de macro, on va utiliser la touche q en mode normal, suivie de la lettre à laquelle on va assigner cette macro. On effectue ensuite les manipulations qu'on va vouloir répéter, puis on va appuyer à nouveau sur q pour enregistrer la macro et revenir en mode normal.

Pour utiliser cette macro, on utilisera (en mode normal), la touche @ suivie de la lettre à laquelle nous avons assigné cette macro. Évidemment, si on précède @ d'un nombre, la macro sera répétée autant de fois que voulu.

Recherche et substitution

Pour effectuer une recherche depuis le curseur vers la fin du fichier (ça boucle donc une fois le dernier terme trouvé en fin de fichier, on repart du début), on utilise la touche / puis on tape son motif de recherche. Les regexes sont prises en compte bien évidemment. Pour effectuer une recherche inverse, on utilise la touche ?.

Pour effectuer une substitution, on utilisera la commande du mode ligne de commande s (donc, 😒):

Commande Action
😒 substitution dans la ligne actuelle
:%s substitution dans tout le fichier
:20,42s substitution de la ligne 20 à la ligne 42

on suffixe cette commande du motif de recherche et du motif de remplacement, en règle générale séparés par le caractère / et on termine par le CommandFlag (ici, gc) Exemple:

# je veux remplacer dans tout le fichier
# tous les mots commençant par Z
# par le terme "bidule"
:%s/\bZ\w+/bidule/gc

Ici, le CommandFlag gc signifie: (g)lobal (c)onfirm. Global indique d'effectuer la manipulation sur tout le buffer (si on l'omet, seule la première occurence sera modifiée), et c demande confirmation à chaque itération.

CommandFlag Action
rien première occurence du motif de recherche
g toutes les occurences
c Demande confirmation à chaque substitution
i insensible à la casse
I force à respecter la casse

De même, si notre motif de recherche utilise le caractère "/", par exemple une liste d'urls, on peut soit l'échapper avec "", soit utiliser un autre motif de séparateur (par exemple "#" ou ";"). À noter que tout ce qui est dit dans ce point s'applique aussi aux substitutions avec le Stream EDitor (sed) qu'on peut être amené à souvent utiliser dans des scripts 😉

Pour aller plus loin.

$ vimtutor

https://vim.swaroopch.com/byte-of-vim.pdf

En Anglais et pour Vim 7 (on est au 8 et des poussières) mais toujours pertinent, un ebook qui vous permettra d'apprendre, et d'approfondir vos connaissances, sur Vim.

Scripting Bash

Hello World!

Ouvrons un fichier que nous appellerons helloworld.sh (original hein?):

#!/bin/bash

echo "Hello, world!"

On sauvegarde, on quitte, et on:

# rend le script exécutable
$ chmod u+x helloworld.sh
# on l'exécute
$ ./helloworld.sh

Notez bien le "./": comme "helloworld.sh" n'est pas dans le PATH, on ne peut l'exécuter qu'en précisant son chemin. On reviendra sur la variable PATH plus loin.

En règle générale, si vous voulez exécuter vos scripts sans avoir à préciser leur chemin, il faut les placer dans le PATH. Deux solutions:

# Linux
/usr/local/bin
#BSD
/usr/local/bin
ou
/opt/bin/

Notez aussi qu'en général, les programmes que vous compilez vous mêmes seront en général placés dans ces répertoires sur un:

$ sudo make install

Si le Makefile est propre, bien sûr.

Mais évitez de les placer dans les répertoires système comme /bin et cie.
Et voilà pour hello world. Trop dur, hein? 😃

Exécution d'un script

Jobs

Pour lancer une commande en arrière-plan, on utilisera le caractère "&" à la fin de la ligne de commande. Exemple, avec vim:

$ vim &
[1] 2604

La commande jobs nous permet de lister les jobs en cours:

$ jobs
[1]+  Stoppé                 vim

Pour ramener vim au premier plan:

$ fg %1

Si on veut remettre Vim en arrière plan, on tapera Ctrl-z dans Vim

Alors c'est un exemple succint, mais on peut imaginer une commande dont l'exécution est particulièrement longue. Dans le cas de Vim, programme interactif, jobs nous indique qu'il est Stoppé, cependant une autre commande non-interactive poursuivra son exécution en arrière plan.

À l'inverse, on peut forcer une commande Stoppée en arrière plan (avec un Ctrl-z, typiquement) à continuer son exécution en utilisant la commande bg, de la même façon que fg.

Enfin, imaginons un script lançant plusieurs commandes "longues" (et potentiellement monothread). Plutôt que de les lancer une par une, on pourrait les lancer toutes ensemble en arrière plan, et utiliser la commande wait pour que le script attende la fin de l'exécution de ses commandes:

#!/bin/bash

commande_super_longue1 &
commande_super_longue2 &
commande_super_longue3 &

wait

suite du script...

De même, on peut utiliser l'instruction:

suspend

Pour suspendre le shell (même effet que Ctrl-z).

Par exemple, ouvrons un shell enfant à notre shell actuel:

$ bash
$ suspend
[1]+  Stoppé                 bash
$ jobs
[1]+  Stoppé                 bash

shebang

Le /shebang/, c'est la combinaison des caractères # et !. Tout script shell commence en général par cette ligne. Elle permet d'indiquer quel interpréteur on veut utiliser pour la suite du script. Exemples:

#!/bin/bash
#!/bin/ksh
#!/usr/bin/python
#!/usr/local/bin/zsh_compilé_a_la_maing

On peut aussi - et c'est même conseillé pour des raisons de portabilités, notamment si votre script est censé pouvoir tourner sur plusieurs distributions GNU/Linux ou d'autres UNIX comme FreeBSD, ou AIX, utiliser cette ligne:

#!/usr/bin/env bash

Qui va interroger les variables d'environnement du système pour déterminer où se trouve l'interpréteur bash, qui peut varier en fonction de l'OS utilisé, exemple:

# Debian GNU/Linux
$ which bash
/usr/bin/bash
#OpenBSD
$ which bash
/usr/local/bin/bash

Tiens, d'ailleurs, on voit que bash n'est pas localisé dans /bin, hors, ça marche dans nos scripts. Comment ça se fait? Vérifions donc ce répertoire:

$ ls -al /bin
lrwxrwxrwx 1 root root 7 août   3  2019 /bin -> usr/bin

Oh, mais... /bin est un lien symbolique en fait sur Debian! Voilà la réponse à notre question!

source

La commande /source/ permet de charger un fichier de fonctions à l'intérieur du script shell courant, ou d'une invite de commande. On peut s'en servir, par exemple, pour provisionner des variables qui seront utilisées dans notre script en effectuant quelques vérifications sur l'OS utilisé, la disponibilité ou non de telle ou telle commande, etc.

Par exemple, créons un premier script (on va l'appeler srctest.sh) qui va provisionner une variable, et vérifier si nous sommes root, ou pas:

#!/bin/bash
PWET=$(date)
is_root() {
  [ $(id -u) -eq 0 ] && return $TRUE || return $FALSE
}

Puis créons un script monscript.sh tout con qui va sourcer notre premier fichier:

#!/bin/bash
source srctest.sh
echo "La valeur de PWET est $PWET"
is_root && echo "bouh, t'es en root" || echo "ok, t'es pas root"

/source/ nous a donc permis de provisionner une variable et d'appeler une fonction depuis un autre script dans notre script, ce qui peut être pratique: imaginons un script nettement plus complexe que le nôtre, il peut être intéressant, pour le rendre plus lisible et plus facile à configurer, de stocker certaines variables et fonctions dans un fichier que l'on sourcera lors de l'exécution de notre méga-script. Et il sera plus simple de modifier certaines variables ou fonctions depuis un fichier plus petit et dédié à ça, plutôt que de retrouver une variable au milieu de 15000 lignes (quoiqu'avec un éditeur comme Vim... :p)

Oh, mais qu'est ce qu'on a fait dans nos deux scripts? on a créé une variable (qui stocke le résultat d'une commande) et on a effectué un test. Ce qui m'amène au prochain chapître, les variables!

Variables

Je ne vais pas vous faire l'insulte de vous expliquer ce qu'est une variable, on va surtout regarder comment elles sont implémentées en Bash 😃

Réécrivons donc notre hello-world de tout à l'heure:

#!/bin/bash

HELLO="Hello World!"
echo $HELLO

Ici, nous venons de créer une variable qu'on pourrait considérer comme étant globale (fear). On peut par contre créer des variables /locales/ à nos fonctions, par exemple, si on modifie (encore) notre Hello World précédent:

#!/bin/bash
HELLO="Hello"
function hello {
  HELLO="World!"
  echo $HELLO
}
echo $HELLO
hello
echo $HELLO

Que se passe-t-il lorsque j'exécute mon script helloworld?

On peut aussi assigner le résultat d'une ou plusieurs fonctions à nos variables (encore heureux), et ce avec deux syntaxes différentes (youpi):

#!/bin/bash
DATE=`date`
DATE2=$(date)

echo $DATE
echo $DATE2

Alors, vous allez me dire: à quoi ça sert d'avoir deux méthodes pour faire la même chose?

En fait, la syntaxe $() est plus récente que la syntaxe utilisant les backticks. Et on peut faire des choses plus rigolotes comme:

#!/bin/bash
PWET=$(echo $(ls)" "$(date))
echo $PWET

ce qu'on ne pourrait pas faire avec les backticks, qui seraient ambigüs du point de vue de l'interpréteur.

Variables spéciales

Bash possède plusieurs variables spéciales.

Variable Signification
$IFS Séparateur (par défaut espace)
$? code de retour du script
$! PID d'un process background (wait)
$$ PID du shell courant
${-} options du shell courant
${_} dernier argument de la dernière commande exécutée

Arguments

Bien sûr, on peut passer des arguments à nos scripts, pour pouvoir les tester ensuite. Mais comment faire en sorte que notre script les reconnaisse? On utilise pour cela les /variables positionnelles/, dont la syntaxe est ${N}, ou N représente la position de l'argument. Si N < 10, on peut omettre les {}.

Essayons donc nos nouveaux amis, dans un script qu'on va nommer args.sh (par exemple):

#!/bin/bash

echo "Premier argument: ${1}"
echo "Second argument: ${2}"

testons notre script!

$ bash arg.sh pwet plop
$ bash arg.sh pwet

Que se passe-t-il lorsqu'on exécute la seconde ligne?

On peut aussi utiliser la variable ${@} pour lister tous les paramètres passés au script - ou à la fonction, on y reviendra plus tard:

#!/bin/bash

echo ${@}

for i in ${@}
do
  echo $i
done

On peut aussi utiliser la variable ${*} pour faire... la même chose, à un détail près: son résultat est influencé par la valeur de la variable IFS vue plus haut. Par exemple:

#!/bin/bash
IFS=";"
echo ${@}
echo ${*}

Quelle est la différence?

Enfin, la variable ${#} permet d'obtenir le nombre d'arguments qui ont été passés au script (ou à la fonction). Typiquement, si votre script demande 3 arguments, on pourra tester la valeur de cette variable et sortir du script si seulement 2 arguments on été passés.

Imaginez un script ou une fonction qui récupère l'id d'un client, son mail, et un flag depuis une base de données. Si le flag est vide, le script supprime l'utilisateur. Si ce troisième argument n'est pas passé au script ou à la fonction, il sera toujours vide, et donc... on supprimera toujours l'utilisateur. Oups. (bon en vrai... on évitera ce genre de flag dans une BDD, c'est pour l'exemple).

getopts

/getopts/ est une fonction interne de bash qui est la version "bashisée" de getopt. Contrairement à /getopt/ qui peut varier entre OS, /getopts/ est définie par le standard POSIX.

getopts nous permet de /parser/ les options courtes passées au script. Vous voyez la commande ls -l ? eh bien -l est une option courte. une option "longue" serait ls --list (qui n'existe pas hein, ne cherchez pas. par contre -L est équivalent à --dereference (liens symboliques)).

getopts peut aussi /parser/ des combinaisons, comme dans ls -al (plus court que ls -a -l). Attention, getopts ne peut pas parser les options longues comme --help.

getopts va parcourir le contenu de la variable ${@} pour extraire de cette chaîne de caractère des options et leurs arguments. Imaginons un script:

$ monscript -a option1 -b adresse_ip

La syntaxe de getopts est celle-ci:

getopts optstring optname [ argument ]

Quand getopts va /parser/ ${@} il va comparer son contenu avec le contenu des options définies dans /optstring/. S'il en trouve une, il la place dans une variable optname. S'il ne matche rien, il placera dans optname le caractère "?".

De même, si l'option nécessite un argument, il sera placé dans la variable $OPTARG. S'il ne trouve pas d'argument alors qu'il devrait y en avoir un, il placera le caractère ":" dans la variable OPTARG.

Puis, getopts incrémente son compteur positionnel $OPTIND, et procède au traitement de l'option suivante.

On peut signifier à getopts la fin des options avec le caractère spécial "--" (d'où le fait qu'il ne prenne pas en compte les options longues).

Lorsque getopts a terminé de traiter les options passées au script, il retourne simplement /false/. Typiquement, on le fera tourner dans une boucle, typiquement une boucle /while/ dans une construction du type:

while getopts options optname [argument]
do
 ...
done

Bon, maintenant, comment ça fonctionne? Imaginons que notre script a besoin de savoir s'il doit par exemple lister (-l) ou copier (-c) le contenu d'un fichier (oui, c'est nul comme exemple, mais c'est simple), ou afficher sa taille (-s, size) sans argument (donc, toujours le même fichier. Par exemple, un fichier de log):

getopts "cls" optname

Maintenant imaginons qu'on aimerait bien passer un argument à copier et lister. Pour se faire, on ajoute le caractère ":" après chacune de nos options ayant besoin d'un argument.

getopts "c:l:s" optname

Imaginons un script - tout pourri - qui va nous permettre de dire bonjour à quelqu'un plusieurs fois (ou une fois par défaut). Oui c'est nul. Mais c'est simple à comprendre 😄.

Notre script aura pour syntaxe:

# pour dire 42 fois coucou à Jean-Eudes
$ coucou -n Jean-Eudes -t 42

On pourrait faire quelque chose du style:

#!/bin/bash
NAME=""
TIMES=1

while getopts "n:t:" options
do
  case "${options}" in
    n) NAME=${OPTARG};;
    t) TIMES=${OPTARG};;
    *) exit 1;;
  esac
done

for i in $(seq 1 $TIMES)
do
  echo "Coucou "$NAME"!"
done

Alors, c'est gentil, mais on ne teste pas plein de conditions possibles dans notre script! Que se passe-t-il si je fais:

$ scriptopt -n Truc -t Pwetpwetlolilol

Comment je pourrais éviter ce problème?

Ce qui nous amène aux tests et conditions en Bash 😃

Tests et conditions

Bien évidemment, Bash nous permet de tester le contenu d'une variable, le retour d'une fonction ou d'une commande, et ainsi de suite. On va distinguer deux grands types de conditions:

Tests et tests à simple crochets

Tout d'abord, contrairement à if..elif..else, /test/ retourne juste un statut de sortie égal à 0 (true) ou 1 (false). Point. On peut l'écrire de la sorte dans un shell:

NUM=un chiffre qui vous plaît, exemple: NUM=755
test $NUM -gt 5 && echo "yep" || echo "nope"

Ici, si le résultat du test est vrai, test va sortir avec une valeur de code de retour égale à zéro. "&&" indique la marche à suivre si la dernière commande est sortie avec un code de retour égal à 0 (i.e. tout s'est bien passé), et "||" indique la marche à suivre si le code de retour de la dernière commande est différent de 0. "-gt" signifie "greater than", plus grand que.

On peut abréger /test/ en mettant notre condition entre crochets:

[ $NUM -gt 5 ] && echo "yep" || echo "nope"

NOTE: les tests à simple crochets sont compatibles avec sh

De même, test nous permettra de tester l'existence de fichiers, répertoires:

$ [ -e /etc/shadow ] && echo "ouf" || echo "oh mer.."
Test Signification
-gt plus grand que
-lt plus petit que
-eq égal
-ne n'est pas égal
-ge supérieur ou égal
-le inférieur ou égal
Test Signification
-a,-e teste si un fichier existe
-b teste si fichier de bloc (genre /dev/sda, fear)
-c fichier de caractères spéciaux (/dev/urandom par ex)
-d teste si le fichier est un répertoire
-f teste si le fichier existe et est un fichier standard
-g teste si le fichier est setgid
-h,-L teste si le fichier est un lien symbolique
-k teste si on a le sticky-bit sur le fichier
-p ce fichier est il un fichier fifo (pipe file)
-r le fichier est il lisible par l'utilisateur?
-s le fichier existe et n'est pas vide
-S le fichier est-il une socket?
-t file descriptor (velu)
-u teste si le fichier est setuid
-w teste si l'utilisateur peut écrire le fichier
-x teste si l'utilisateur peut exécuter le fichier
-O teste si le fichier appartient à l'user
-G teste si le fichier appartient au groupe de l'utilisateur
-N teste si le fichier a été modifié depuis sa dernière lecture (atime)
f1 -nt f2 teste si f1 est plus récent que f2
f1 -ot f2 f1 est-il plus vieux que f2?
f1 -ef f2 f1 est il un lien en dur de f2 (hardlink)
test -n chaîne teste si la chaîne n'est pas vide
-z true si la chaîne testée est vide
chaine1 = chaine2 teste si chaine1 est identique à chaine2
chaine1 != chaine2 true si chaine1 est différente de chaine2
"chaine1" < "chaine2" velu: teste si chaine1 est sortie lexicographiquement avant chaine2
"chaine1" > "chaine2" idem mais dans l'autre sens 😃
-o option vrai si l'option shell /option/ est activée
-v variable vrai si la variable shell est /set/
-R variable velu, vrai si la variable est /set/ ET que c'est une référence (directe ou indirecte
! expression vrai si expression est nulle
expr1 -a expr2 vrai si l'expression 1 et l'expression 2 sont nulles
expr1 -o expr2 vrai si une des deux expressions n'est pas nulle

De même, il est possible d'utiliser le ET et le OU logiques dans un test, respectivement avec les arguments -a et -o:

[ $num -lt 1 -o $num -eq 100 ] && echo "num est soit inférieur à 1 soit égal à 100" || echo "num est entre 1 et 100"
[ $num -lt 1 -a $num -eq 100 ] || echo "jamais vrai!"

Tests à doubles crochets

Contrairement aux tests à simple crochets, les tests à double crochets ne sont pas compatibles avec sh mais elles permettent de faire plus de choses 😃, par exemple utiliser le wildcard:

[[ "$nom" == *yolo* ]] && echo "contient bien yolo" || echo "pas de yolo"

Et permettent aussi d'utiliser les expressions régulières 😃

nom="thierry"
[[ "$nom" == [tT]hierry ]] && echo "it's me" || echo "who the fuck are you?"

Conditions

Alors on peut déjà faire beaucoup de choses avec /test/, mais avec les opérateurs if, elif et else, on peut combiner ces conditions afin d'effectuer des tests bien plus complexes:

#!/bin/bash
if [ -f ./thisisatest ]
then
  echo "le fichier existe"
elif [ -d ./thisisatest ]
then
  echo "le répertoire existe"
else
  echo "je cree le fichier"
  touch thisisatest
fi

bien sûr, on peut mettre autant de /elif/ que l'on veut.

Un autre de type de condition est le test case, que nous avons déjà utilisé:

#!/bin/bash

echo "votre nom:"
read answer
case $answer in
  Gates ) echo "Retourne sous Windows!";;
  Torvalds ) echo "NVidia, F*** you!";;
  *) echo "mais t'es qui toi?";;
esac

Calcul

Opérateurs arithmétiques

Opérateur Signification
var++,var-- post-incrémente/décrémente la variable
++var,--var pré-incrémente/décrémente la variable
!,~ négation logique, négation bit à bit
** puissance
*,/,% multiplication, division, modulo
+,- addition, soustraction
expr ? expr opérateur conditionnel

Attention, il en existe d'autres, je reste dans le conventionnel ici.

Calcul sur des entiers

Quatre possibilités:

# expr
$ expr 1 + 1
$ expr $var / 3
$ expr $var \* 10
# let
$ let x=3 y=42 z=x*y a=z**2
$ echo $x $y $z $a
# declare
$ declare -i x=3 y=42 z=x*y a=z**2
$ echo $x $y $z $a
# expansion
$ num=3
$ echo $((num+2))
$ moarnum=$((num*4))
$ ((num+=3))
$ echo $((num++))
$ ((x=3,y=42,z=x*y,a=z**2)); echo $x $y $z $a

Calcul sur des flottants

Trois solutions:

Concernant printf, on va préciser... hum, la précision justement, par exemple ici 3 chiffres après la virgule, puis le multiplieur et enfin l'exponentielle inverse du multiplieur. yeah, ça vend du rêve:

# ze formule, mult=multiplier
$ printf %.<precision>f "$((10**<mult> * 2/3))e-<mult>
# calculer 2/3 à 3 chiffres après la virgule:
$ printf %.3f "$((10**3 * 2/3))e-3"
# calculer 2/3 à 6 chiffres après la virgule:
$ printf %.6f "$((10**6 * 2/3))e-6"
0.666

AWK - écrit entre autre par Brian Kernighan:

$ awk "BEGIN {print 2/3}"
$ awk "BEGIN {x=2/3; y=42; z=x*y; print z}"

Et enfin bc:

$ sudo apt install bc
$ echo "2/3" | bc
0
# oups!
$ echo "scale=3;2/3"|bc
0.666
$ bc <<< "scale=3;2/3"
0.666

Bon, soyons francs: Bash n'est pas vraiment fait pour faire des maths. Utilisez donc /bc/ ou /awk/ pour tout ce qui est plus compliqué que des opérations sur les entiers 😉

Boucles

Là encore, comme pour les variables, je ne vais pas vous faire l'insulte de vous expliquer ce qu'est une boucle 😃

for

la boucle /for/ nous permet d'itérer sur une /liste/. Par exemple:

#!/bin/bash

for i in $(seq 1 10)
do
  echo $i
done

On peut aussi, plutôt que d'utiliser le vieil utilitaire /seq/, utiliser un intervalle:

#!/bin/bash

for i in {1..10}
do
  echo $i
done

De même, on peut préciser un pas à l'intervalle, par exemple ici, compter de 10 à 0 de 2 en 2:

#!/bin/bash

for i in {10..0..2}
do
  echo $i
done

On peut aussi itérer sur le contenu d'un fichier ou d'une variable:

#!/bin/bash

liste_ips="10.10.0.1 10.10.0.2 10.10.0.3"

for i in $liste_ips
do
  echo "je vais ban "$i
done

Et bien sûr sur le résultat d'une commande:

#!/bin/bash

for i in $(tail -100000 /var/log/haproxy.log | grep "wp_admin" | awk '{ print $6 }' |\
          sed 's/:.*$//g' | sort -n | uniq)
do
  iptables -I INPUT -s $i -j DROP
done

à votre avis, qu'est ce qu'on fait là? Indice: c'est très très bourrin 😃

De même, on peut sortir d'une boucle si une condition est vérifiée avec l'instruction /break/:

#!/bin/bash

for i in ${1..10}
do
  if [ $i -eq 8 ]
  then
    break
  fi
  echo $i
done

(Bon, là, ça ne sert pas à grand chose, mais imaginons qu'on teste l'espace disque dispo sur le backup, etc)

à l'inverse, on peut utiliser l'instruction /continue/ pour dire à un script d'ignorer l'itération en cours et de continuer à la prochaine:

#!/bin/bash

for i in ${1..10}
do
  if [ $i -eq 8 ]
  then
    continue
  fi
  echo $i
done

/Question: dans les deux boucles précédentes, que se passe-t-il si je place le echo avant la condition?/

les boucles /for/ sont aussi très intéressantes pour effectuer des opérations sur, par exemple, un ensemble de fichiers, en utilisant les /wildcards/:

#!/bin/bash

REP=$HOME/org

for i in $REP/*.org
do
  pandoc $i -t latex -o $i.pdf
done

while et until

Les boucles /while/ ("tant que") et /until/ ("jusqu'à ce que" - la différence est importante) nous permettent d'exécuter un bloc de commandes tant qu'une condition n'est pas vérifiée, ou jusqu'à ce qu'elle soit vérifiée. On peut aussi facilement créer des boucles infinies avec while:

#!/bin/bash

while true
do
  echo "I'll never stop"
  sleep 5s
  echo "Ctrl-c pour quitter"
done

Maintenant, faisons un test un chouilla plus compliqué:

#!/bin/bash

compteur=1
while [ $compteur -lt 10 ]
do
  echo $compteur
  ((compteur++))
done

avant de l'exécuter, à votre avis quel sera le résultat?

/until/ fonctionne de façon très similaire, par exemple:

#!/bin/bash

compteur=1
until [ $compteur -gt 10 ]
do
  echo $compteur
  ((compteur++))
done

Quel sera le résultat?

En vrai, on peut tout à obtenir le même résultat avec un /while/ - Quel serait-il à votre avis?

select

On peut utiliser l'instruction /select/ pour créer un menu simple:

#!/bin/bash

player="Ken Ryu Blanka Zangieff Quit"

PS3='Select your player: '

select i in $player
do
  if [[ $i == 'Quit' ]]
  then
    break
  fi
  echo Hello $i
done

Fonctions

On l'a déjà vu, on peut créer une fonction de deux façons différentes:

nom_fonction() {
  commandes
}

# Ou

function mafonction {
  commandes
}

Les deux méthodes sont identiques, c'est juste une question de préférence personnelle 😃

On peut bien sûr (et c'est l'intérêt) passer des /arguments/ à notre fonction:

#!/bin/bash

function write_sthg {
  echo Hello $1
}

write_sthg World

On peut aussi dire à la fonction de retourner un code de sortie (afin de pouvoir l'analyser avec des tests sur la valeur de $? par exemple):

#!/bin/bash

function write_sthg {
  echo Hello $1
  return 42
}

write_sthg World
echo $?

en règle générale, un code de retour égal à zéro signifie que tout s'est bien déroulé. Un code de retour différent peut être généré pour différents types d'erreur. De même, /return/ peut prendre la valeur d'une variable.

On a vu dans le chapître sur les variables que l'on pouvait assigner des variables locales à une fonction, donc je ne vais pas m'étendre là-dessus 😃

Tableaux

Aaah, les tableaux en bash... hum. On peut créer:

# tableau indexé:
declare -a ma_table
# tableau associatif
declare -A ma_table
$ ma_table=(plop pwet)
#ou en initialisant la première cellule du tableau
$ ma_table[0]=plop
$ echo ${ma_table[@]}
# Ou
$ echo ${ma_table[*]}

Les deux variantes sont équivalentes sauf si on les utilise entre quotes et double quotes:

$ ma_table=(pwet plop)
$ for i in "${ma_table[@]}"; do echo $i; done
$ for i in "${ma_table[*]}"; do echo $i; done

Quelle est la différence entre les deux résultats?

#!/bin/bash
# tableau indexé

ma_table=(foo bar baz)

for i in "${!my_array[@]}"
do
  echo $i
done
#!/bin/bash
# tableau associatif
declare -A ma_table
ma_table=([foo]=bar [baz]=foobar)

for k in "${!ma_table[@]}"
do
  echo $k
done
$ echo ${#ma_table[@]}

Si le tableau est un tableau indexé, c'est très simple:

$ ma_table=(plop pwet)
$ ma_table+=(blip)
# On peut aussi ajouter plusieurs éléments
$ ma_table+=(blop ping)

Si le tableau est un tableau associatif, il faut bien sûr préciser la clef associée:

$ declare -A ma_table
$ ma_table[pwet]="plop"
$ ma_table+=([blip]=blop [ping]=pong)

Dans le cas d'un tableau indexé:

$ ma_table=(plop pwet blip)
# on efface le second élément
$ unset ma_table[1]
$ echo ${ma_table[@]}
$ echo ${!ma_table[@]}

Tiens, que remarquez-vous concernant nos indexs?

Dans le cas d'un tableau associatif, on supprime la clef:

$ declare -A ma_table
$ ma_table+=([pwet]=plop [blip]=blop [ping]=pong)
$ unset ma_table[blip]
$ echo ${ma_table[@]}
$ echo ${!ma_table[@]}

Enfin, pour supprimer tout le contenu d'un tableau:

$ unset ma_table
$ echo ${!ma_table[@]}

Et on a fait le tour des tableaux 😃

Netfilter/iptables

Présentation

Définitions

Netfilter est un /framework/ implémentant un pare-feu au sein du kernel Linux, et ce depuis sa version 2.4.

netfilter.png

/source: Wikipedia, Jan Engelhardt/

iptables est un utilitaire permettant de manipuler les différentes tables gérées par netfilter de façon plus "humaine" (ça se discute :p)

Netfilter fonctionne sur les couches 2,3,4 du modèle OSI, et les couches 1,2 et 3 du modèle TCP/IP. Il utilise les modules kernel suivants:

ip_tables tables IPv4
ip6_tables tables IPv6
arp_tables tables ARP
ebtables filtrage (firewall)

On peut manipuler ces tables avec les utilitaires /iptable/, /ip6tables/, /arptables/, et /ebtables/.

Chaines

la table Filter

La table filter est la table qu'on affichera par défaut sur un:

$ sudo iptables -L
# en la spécifiant
$ sudo iptables -t filter -L -n --line-numbers

C'est aussi sur cette table qu'on agira par défaut - après tout, c'est cette table qui va nous servir à configurer nos règles de /firewalling/.

Par exemple, si on veut agir sur la table NAT, il faudra le préciser dans notre ligne de commande:

# du NAT, the easy way, entre eth0 et eth1
# il faudra aussi configurer la machine en mode routeur dans sysctl.conf :)
$ sudo iptables -t nat -A POSTROUTING -i eth0 -o eth1 -j MASQUERADE

Utilisation

Stratégies de filtrage

Par défaut, iptables utilise la stratégie ACCEPT sur ses chaînes. Cela signifie que tout ce qui n'est pas explicitement interdit (DROP) sera accepté. Typiquement, sur un environnement de production, on ne voudra autoriser en entrée que le trafic sur les ports dont nous avons besoin, par exemple les ports 80, 443 sur un serveur Web, et bien sûr le port qu'on utilise pour SSH (par défaut le 22), car on ne voudrait pas s'enfermer dehors.

On va donc vouloir faire l'inverse: ce qui n'est pas explicitement autorisé est implicitement interdit, en changeant la stratégie de filtrage d'iptables/netfilter.

Dans notre cas, si nous voulons autoriser uniquement le trafic sur les ports 22, 80 et 443, et configurer iptables pour DROP tout le reste, on utiliserait les règles suivantes:

# on autorise le trafic sur ces ports sur notre interface eth0 
# (ou ens18, ou enps2f0, eno1, etc suivant le matériel utilisé)
$ sudo iptables -A INPUT -p tcp -i eth0 --dport 22 -j ACCEPT
$ sudo iptables -A INPUT -p tcp -i eth0 --dport 80 -j ACCEPT
$ sudo iptables -A INPUT -p tcp -i eth0 --dport 443 -j ACCEPT
# on passe en policy DROP par défaut
$ sudo iptables -P INPUT DROP

Maintenant, essayez de ping votre VM de test, que se passe-t-il?

Ajout de règles

On peut donc aussi vouloir, dans cette configuration, autoriser les requêtes ICMP (ping), juste pour vérifier que la machine est UP. Pour cela, on va devoir ajouter une règle autorisant l'ICMP. Pour ajouter une règle iptables à une chaîne d'une table (en l'occurence la table filter, ici), on a vu le commutateur -A, /append/, qui ajoute la règle à la suite des autres règles déjà définies. La règle est ici très simple:

$ sudo iptables -A INPUT -p icmp -j ACCEPT

On peut aussi vouloir insérer une règle avant les autres, ou à une position précise, avec l'option -I (/Insert/), de la forme:

# avant tout le monde
$ sudo iptables -I INPUT -p icmp -j ACCEPT
# en position 3
$ sudo iptables -I INPUT 3 -p icmp -j ACCEPT

Bon, maintenant, on peut bien ping notre machine. Peut-elle ping, au hasard, google.fr? 8.8.8.8? Donc, la résolution DNS est pétée 😃

Pourquoi? parcequ'on accepte uniquement les paquets en entrée /à destination/ des ports 22, 80, 443. Mais on accepte pas les paquets réponse DNS en provenance du port 53 de notre résolveur DNS. DNS utilise UDP, donc, on veut autoriser les paquets UDP dont le port source d'émission (i.e. le port du serveur DNS) est le port 53:

$ sudo iptables -A INPUT -p udp --sport 53 -j ACCEPT

Et on devrait pouvoir DDoS tranquillement google.fr en le "pingant" (enfin, DDoS... lol)

Par contre, le petit souci de la policy DROP, c'est que même les communications sur l'interface /loopback/ sont interdites 😃

On va donc les autoriser, ça peut être pratique...

$ sudo iptables -A INPUT -i lo -j ACCEPT

Tiens, essayons d'installer un paquet sur notre distrib', au hasard, curl. Que se passe-t-il?

En fait, tout comme pour la résolution DNS tout à l'heure, on autorise pas le trafic de retour en provenance des ports 80 et 443. Comment va-t-on les autoriser?

Maintenant, imaginons un scénario de type DDoS. Le sous-réseau public d'un de nos concurrents (au hasard, un concurrent du pays imaginaire d'Al-Quolanudar (pour les fans de /What we do in the Shadows/. Sacré Nandor.) dont le sous réseau est 109.110.12.0/17. Afin de sauvegarder notre business, on va tout simplement l'envoyer paître:

$ sudo iptables -A INPUT -s 109.110.12.0/17 -j DROP

Et voilà, problème résolu.

Suppression de règles

Comment supprime-t-on une règle iptables?

Deux solutions:

$ sudo iptables -L -n --line-numbers
# on repère le numéro de la règle à supprimer, par ex 12 ici:
$ sudo iptables -D INPUT 12

Ou sinon, il faut taper exactement la règle en remplaçant -A ou -I par -D:

$ sudo iptables -D INPUT -s 109.110.12.0/17 -j DROP

On peut aussi flusher le filtre et toutes les tables:

ATTENTION si vous êtes en policy DROP, ça va aussi supprimer les règles qui autorisaient SSH, web, etc. Et donc, vous allez vous enfermer dehors!

# on évite de s'enfermer dehors!
$ sudo iptables -P INPUT ACCEPT
# on flushe!
$ sudo iptables -F
# on delete toutes les chaînes utilisateur
$ sudo iptables -X

conntrack

Conntrack, /Connection Tracking/ permet au kernel de conserver la trace de toutes les sessions et connexions (logiques) et donc de tous les paquets qui composent une connexion. NAT utilise conntrack, et iptables peut utiliser ces informations pour agir comme un pare-feu persistant.

Conntrack stocke donc l'état des connexions et permet à iptables d'agir en fonction de ces états, qui sont:

Pour ne pas risquer de couper une connexion déjà existante sur un flush/reload/modification de nos règles iptables, on va se servir de l'état ESTABLISHED de la connexion dans conntrack et créer une règle iptables correspondantes:

$ sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED -j ACCEPT

On peut aussi s'en servir de protection syn-flood:

$ sudo iptables -A INPUT -p tcp ! --syn -m conntrack --ctstate NEW -j DROP

Bonus

Il peut être intéressant d'ajouter des commentaires à nos règles iptables, afin de les reconnaître facilement lorsqu'on les liste. Si je reprends la règle de ban d'un subnet de tout à l'heure:

$ sudo iptables -A INPUT -s 109.110.12.0/17 -j DROP -m comment --comment "/* Al-Quolanudar ban */"

On peut aussi mettre en place du /rate-limiting/ par IP (ici, 5 paquets par seconde - potentiellement violent hein):

$ sudo iptables -A INPUT -i eth0 -p tcp --syn -m limit --limit 5/s -j ACCEPT 
# en UDP - pas de SYN en UDP (fire and forget)
$ sudo iptables -A INPUT -i eth0 -p udp -m limit --limit 5/s -j ACCEPT 

On peut bien sûr faire du NAT un peu plus propre que mon exemple précédent, par exemple avec un VPN L3 dont le subnet serait 10.10.10.0/24:

$ sudo iptables -A FORWARD -i eth0 -o tun0 -m conntrack -ctstate ESTABLISHED -j ACCEPT
$ sudo iptables -A FORWARD -s 10.10.10.0/24 -o eth0 -j ACCEPT
$ sudo iptables -t nat -A POSTROUTING -s 10.10.10.0/24 -o eth0 -j MASQUERADE

Bien évidemment, on peut aussi, assez souvent, vouloir bannir les sous-réseaux d'un AS complet. Pour cela il faut obtenir la liste de ses sous-réseaux depuis une DB whois reverse (typiquement radb.net):

$ whois -h whois.radb.net -- '-i origin AS24940' | grep route |\
 egrep -v "(route6|descr|mnt)" | awk '{ print $2 }' > hetzner_subnets
# hetzner est un hébergeur allemand comme OVH/Scaleway/DigitalOcean/Vultr/etc
$ for i in $(cat hetzner_subnets); do sudo iptables -I INPUT -s $i -j DROP; done

Sauvegarder et restaurer des règles iptables

On a plusieurs moyens, par exemple le paquet /iptables-persistent/.

Une autre solution est le couple /iptables-save/ et /iptables-restore/:

$ sudo iptables-save | sudo tee /etc/iptables.rules
$ sudo iptables-restore < /etc/iptables.rules

Et pour le rappeler au boot, on peut soit créer une unité systemd, soit les réinjecter à l'UP d'une interface réseau:

$ sudo vim /etc/network/interfaces
...
iface ens18 inet static
   address 10.0.0.1/24
   gateway 10.0.0.254
   up iptables-restore < /etc/iptables.rules

Outils réseau

netstat et ss (erf)

Pour regarder les connexions établies, les sockets et streams UNIX en cours d'utilisation, on dispose de deux utilitaires:

$ sudo netstat -tpan
Connexions Internet actives (serveurs et établies)
Proto Recv-Q Send-Q Adresse locale          Adresse distante        Etat        PID/Program name                                                              
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      410/sshd                                                                      
tcp        0      0 0.0.0.0:111             0.0.0.0:*               LISTEN      1/init                                                                        
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      9245/apache2                                                                  
tcp        0     68 192.168.1.198:22        192.168.1.197:42660     ESTABLISHED 5029/sshd: lidstah                                                            
tcp6       0      0 :::22                   :::*                    LISTEN      410/sshd                                                                      
tcp6       0      0 :::111                  :::*                    LISTEN      1/init                                                                        

$ sudo ss -tanp
State   Recv-Q  Send-Q     Local Address:Port      Peer Address:Port                                                                                           
LISTEN  0       128              0.0.0.0:22             0.0.0.0:*       users:(("sshd",pid=410,fd=3))                                                          
LISTEN  0       128              0.0.0.0:111            0.0.0.0:*       users:(("rpcbind",pid=314,fd=4),("systemd",pid=1,fd=78))                               
LISTEN  0       128              0.0.0.0:80             0.0.0.0:*       users:(("apache2",pid=9247,fd=3),("apache2",pid=9246,fd=3),("apache2",pid=9245,fd=3))  
ESTAB   0       68         192.168.1.198:22       192.168.1.197:42660   users:(("sshd",pid=5035,fd=3),("sshd",pid=5029,fd=3))                                  
LISTEN  0       128                 [::]:22                [::]:*       users:(("sshd",pid=410,fd=4))                                                          
LISTEN  0       128                 [::]:111               [::]:*       users:(("rpcbind",pid=314,fd=6),("systemd",pid=1,fd=80))

netcat

Netcat, a.k.a /nc/, est une sorte de couteau suisse. On peut faire plein de choses avec:

$ nc mail.server.tld 25
$ nc -v -n -z -w 1 192.168.1.198 1-100

# sur le "serveur"
$ nc -l -p 6667
# sur le client
nc ip_serveur 6667
# sur le receveur:
$ nc -l -p 1234 > file
# sur l'émetteur: (-w pour timeout après 3 secondes sans rien envoyer)
$ nc -w 3 ip_receveur 1234 < file
# sur le serveur:
nc -l -p 1234 -e /bin/bash
# sur le client, si l'ip du serveur est 192.168.1.100
nc 192.168.1.100 1234
ls -al
...

nmap

nmap nous permet de scanner un hôte, un sous-réseau, etc, à la recherche de ports ouverts. Un peu ce qu'on vient de faire avec netcat mais en nettement plus intéressant. En général il est aussi intéressant d'activer le mode verbose³ (-vvv) pour avoir beaucoup d'informations:

$ nmap -A -T4 -vvv 192.168.1.198
Starting Nmap 7.80 ( https://nmap.org ) at 2020-09-16 18:54 CEST
NSE: Loaded 151 scripts for scanning.
NSE: Script Pre-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 18:54
Completed NSE at 18:54, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 18:54
Completed NSE at 18:54, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 18:54
Completed NSE at 18:54, 0.00s elapsed
Initiating Ping Scan at 18:54
Scanning 192.168.1.198 [2 ports]
Completed Ping Scan at 18:54, 0.00s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 18:54
Completed Parallel DNS resolution of 1 host. at 18:54, 0.00s elapsed
DNS resolution of 1 IPs took 0.00s. Mode: Async [#: 2, OK: 0, NX: 1, DR: 0, SF: 0, TR: 1, CN: 0]
Initiating Connect Scan at 18:54
Scanning 192.168.1.198 [1000 ports]
Discovered open port 111/tcp on 192.168.1.198
Discovered open port 22/tcp on 192.168.1.198
Discovered open port 80/tcp on 192.168.1.198
Completed Connect Scan at 18:54, 0.06s elapsed (1000 total ports)
Initiating Service scan at 18:54
Scanning 3 services on 192.168.1.198
Completed Service scan at 18:54, 6.01s elapsed (3 services on 1 host)
NSE: Script scanning 192.168.1.198.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 18:54
Completed NSE at 18:54, 0.18s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 18:54
Completed NSE at 18:54, 0.01s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 18:54
Completed NSE at 18:54, 0.00s elapsed
Nmap scan report for 192.168.1.198
Host is up, received syn-ack (0.0031s latency).
Scanned at 2020-09-16 18:54:05 CEST for 6s
Not shown: 997 closed ports
Reason: 997 conn-refused
PORT    STATE SERVICE REASON  VERSION
22/tcp  open  ssh     syn-ack OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey: 
|   2048 26:b4:6a:3b:8b:57:84:80:14:e7:8b:a5:03:66:0b:23 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3W9fkhGRcAquYowXIDvYQbo9ZCdaQJfXAcGGafASLCRvCaCXM+S/iEMFTad+5w7pb/LY8UFaF85ypoPyL/+hJLSNP/pX+rUjVynDTyTduFtv7rTlnOv03uhKf09NJsQT39H6i3Mx3uw6+8stJwEeeK+POnvPdHt4Ba5jUqkpi7cLdwbGk4MmxJ23rFVTbJvaO/3UcI6vbHP6kyW3IkT17mGGp60AD7nIUIJvVExaMPhjD7ILQXXxzUdSQ4tORnlPqcP3u2QqNEFNjSr4hf/elnSoluMjfqZwH/Zhb3Xh2MhF9irjDex9Njx/ZU7YH01xASjWcyMS1Z14ECu3SbU4L
|   256 68:9a:9f:dd:1f:f0:91:aa:a4:ed:69:a0:dc:ea:36:23 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIhbFwqgkc8FfAxwG+iG/l41MeHqAHroIJeauX7EqVZbk3li5VzI3QJTvJyYecma4C2SGgZtUpTwSGFz1iRLATg=
|   256 18:bc:ba:66:6a:a2:85:9e:34:9e:84:ea:85:ff:de:a7 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ5CKS7vdszGdPrHf/Qchf1WatFGGLC3pjr+4nAvzKWx
80/tcp  open  http    syn-ack Apache httpd 2.4.38 ((Debian))
| http-methods: 
|_  Supported Methods: OPTIONS HEAD GET POST
|_http-server-header: Apache/2.4.38 (Debian)
|_http-title: Apache2 Debian Default Page: It works
111/tcp open  rpcbind syn-ack 2-4 (RPC #100000)
| rpcinfo: 
|   program version    port/proto  service
|   100000  2,3,4        111/tcp   rpcbind
|   100000  2,3,4        111/udp   rpcbind
|   100000  3,4          111/tcp6  rpcbind
|_  100000  3,4          111/udp6  rpcbind
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

NSE: Script Post-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 18:54
Completed NSE at 18:54, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 18:54
Completed NSE at 18:54, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 18:54
Completed NSE at 18:54, 0.00s elapsed
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 6.73 seconds

Bien sûr, je peux aussi nmap tout un subnet:

$ nmap -A -T4 -vvv 192.168.1.0/24
# je vous passe le résultat :D

Nmap est un logiciel extrêmement complet - et complexe - je vous recommande donc chaudement la lecture de la /manpage/ ainsi que la doc' sur le site officiel https://nmap.org

tcpdump, tshark, WireShark

Ceux qui étaient en cours réseau l'an dernier doivent se rappeler de Wireshark. tcpdump est un peu son ancêtre, et tshark son implémentation en mode texte. Ces logiciels permettent d'écouter le trafic réseau d'une interface et de le /dumper/ soit dans la sortie standard (stdout), soit dans un fichier, soit dans un clicodrome, etc.

Ils peuvent être utiles pour détecter différents types de problèmes - Cependant évitez de dumper tout le trafic réseau d'une interface réseau WAN d'une grosse plateforme Web sans filtrer un minimum (src, port, etc) car ça risque vite d'être illisible 😃

TP: mise en oeuvre sur Debian

Pour ce TP, votre groupe va avoir besoin:

Step 1: Secure that stuff! (6 points)

Préparez un script qui prendra en compte en arguments les ports autorisés sur l'interface primaire (eth0, ens18, etc) de vos deux VMs et provisionnera les règles iptables correspondantes. Répondez aux questions suivantes:

Step 2: "Parsage" des logs et exports BDD (4 points)

Préparez un, ou plusieurs scripts, qui vont effectuer les tâches suivantes en parsant les logs Apache et en effectuant des requêtes MySQL:

On va vouloir parser les logs Apache2 de façon à extraire:

Côté base de données:

Step 3: Ban all the lamers! 10 points

Sur notre serveur Web, que nous allons considérer comme notre frontal, on va vouloir bloquer automatiquement les utilisateurs "malveillants", c'est à dire:

On va vouloir les bannir via iptables, pendant un intervalle de temps donné (on prendra 1h dans notre cas).