Visualiser une révision

Une introduction au contrôle du trafic réseau avec Linux

nicolas : révision n°2 (26 février 2011 14:53:52)

_Les débits des flux réseau peuvent être contrôlés ou rendus prioritaires en fonction des en-têtes des paquets IP qui transitent, par exemple : les adresses de la source ou de la destination ainsi que les ports.  Les serveurs s’en servent pour faire de la _Quality of Service_ (QoS) et assurer par exemple un débit minimal aux services en _streaming_._

_J’ai eu l’idée saugrenue de mettre en place de la QoS pour une connexion ADSL haut-débit telle que fournie aux particuliers.  J’y ai gagné :  
— une réactivité plus importante, nécessaire aux sessions intéractives ssh ;  
— un débit auto-régulé entre les différents services, le _streaming_ prioritaire est nécessaire pour éviter les coupures intempestives (que ce soit la télévision de mon FAI que je reçois ou de la musique que je diffuse), tandis que les téléchargements de fond n’occuperont que la bande passante disponible ;  
— une bonne migraine, il faut dire que j’ai appris sur le tas, milles excuses pour les approximations et erreurs qui n’auront pas manqué de se glisser, le journal est long : j’ai tenté autant que possible d’aller du général vers le technique, les parties « Configuration du réseau » et suivantes sont pour ceux qui voudraient mettre en place de la QoS._

_Par contre cette solution miracle nécessite de se limiter à une bande passante d’environ 90 % celle disponible._

Introduction
========

Imaginons que je commence le téléchargement de [Sintel](http://www.sintel.org/wp-content/content/download.html), j’utilise toute la bande passante, mettons 800 ko/s.  Ensuite je veux commencer à regarder la télévision, la solution simple serait d’avoir un logiciel de téléchargement qui permet de brider la vitesse à 200 ko/s pour avoir assez de bande passante pour le reste.  Avec la QoS, sans intervention de l’utilisateur, le flux de la télévision sera prioritaire et utilisera la bande passante nécessaire, la vitesse de téléchargement du film s’adaptera alors au débit laissé libre.

Autre cas d’utilisation : j’ai installé MPD qui peut diffuser la musique jouée sur le port 8000 en http.  Mais pas question d’avoir de coupures sur le [concerto pour violon de Sibelius](http://fr.wikipedia.org/wiki/Concerto_pour_violon_de_Sibelius) ! même si je lance une session scp.  Or je veux toujours que ssh et scp soit prioritaire, je pourrais donc assurer un débit minimal pour la musique.

Au passage, vous faites déjà de la QoS sans le savoir si vous utilisez ssh et scp.  Ces outils renseignent correctement le champ TOS qui se trouve dans les en-tête des paquets (et fait pour!), ssh spécifiera qu’il faut minimiser les délais, scp se contentera de maximiser les débits.  Linux donnera la priorité aux paquets qui veulent minimiser les délais.

Écrire des tc avec classe
================

tc est le programme en ligne de commande qui permet ces petits miracles.  L’outil permet de créer des gestionnaires (que les barbares d’outre-manche appelleront _qdisc_), sorte d’algorithme qui se chargeront de sélectionner le moment et quels paquets auront l’honneur de rejoindre la carte réseau (qui les enverra ensuite vers le grand’ternet).  Certains gestionnaires peuvent avoir des classes, genre de liste d’attente qui stockeront les paquets à envoyer, le gestionnaire se charge alors de sélectionner la classe qui pourra émettre un paquet.  La puissance de tc réside dans le fait qu’on peut attacher un autre gestionnaire à une classe fille d’un gestionnaire déjà existant, on a au final ce qui s’apparente à un arbre.  Enfin on définit des filtres pour dire dans quelle classe doivent être mis en attente les paquets à envoyer.

Pour résumer : une application transmet des données, le noyau prépare le paquet, le filtre et le place dans la file d’attente appropriée, le gestionnaire vide cette file d’attente et envoie sur la carte réseau en fonction de son algorithme interne.  C’est _grosso modo_ le fonctionnement général, de ce que j’ai compris.

Configuration du réseau
================

_Il faudra être Dieu sur votre machine (_aka_ root, mais ça en jette moins) pour tout ce qui va suivre._

Vous aurez remarqué… je veux dire la moitié que je n’ai pas encore perdue (ça va venir, niak!)… bref… vous aurez remarqué que je n’ai parlé que d’envois de paquets dans le paragraphe précédent.  On ne peut tout simplement pas contrôler ce qu’on reçoit.  Je sais, pour moi aussi ça a été une grosse déception.  Mais il y a une solution !

Il faut avoir un routeur.  Mettons qu’il est connecté sur Internet grâce à l’interface réseau eth0 et connecté au réseau local avec eth1, on pourra contrôler ce qu’envoie eth0 et ce qu’envoie eth1.  La magie des connexion réseau permettra alors de réguler le débit aval et amont entre Internet et le réseau local.  En fait l’interface eth1 enverra au débit régulé, comme le noyau ne peut pas garder une quantité infinie des paquets qui arrivent sur eth0 certains iront sur eth1 mais d’autres seront tout simplement ignorés.  Ignorer un paquet revient tout simplement à faire croire au serveur émetteur qu’il a été perdu à cause d’une connexion réseau trop faible, il adaptera alors automatiquement le débit d’envoi des paquets à ce que vous aurez choisi sur eth1 (c’est un mécanisme général, pour TCP si je ne dis pas de connerie).  Ceux qui ont un routeur, contenter vous d’initialiser la variable shell DEV à votre interface externe (eth0 dans mon exemple), et la variable IFB doit contenir le nom de l’interface interne (eth1).

Pour les autres qui ont une *box et un seul ordinateur (dont votre serviteur), ne courrez pas acheter un routeur !  On suppose que vous ayez mis le nom de votre interface réseau (en général eth0) dans une variable shell nommée DEV, on va créer une interface réseau fantôme : 
```bash
IFB=ifb0
modprobe ifb
ip link set dev $IFB up
tc qdisc add dev $DEV ingress
tc filter add dev $DEV parent ffff: protocol ipv6 u32 match ip6 dst ::/128 flowid 1:1 action mirred egress redirect dev $IFB
tc filter add dev $DEV parent ffff: protocol ip u32 match ip dst 0/0 flowid 1:1 action mirred egress redirect dev $IFB
# Pour l’IPv6 il suffit de faire quelques remplacements
# tc filter add dev $DEV parent ffff: protocol ipv6 u32 match ip6 dst ::/128 flowid 1:1 action mirred egress redirect dev $IFB
```

Les gestionnaires, exemple d’application avec SSH
=================================
_Où on a besoin de l’aspirine_

HTB
-------

Nous allons pouvoir attaquer le contrôle proprement dit.  Le gestionnaire racine sera de type _Hierarchical Token Bucket_ (HTB), le premier qu’on verra, et le plus complexe (c’est mon côté diabolique).  Il permet une gestion avancée de la bande-passante (débit minimal garanti, débit maximal, etc.), mais je ne m’en sers que pour limiter la bande-passante à 90 % de celle à disposition.  Je rapelle que cette limitation en-dessous des capacités est primordiale si vous voulez que ça fonctionne !  En effet, la liste d’attente sera nécessairement sur l’interface réseau la plus lente, si vous voulez pouvoir contrôller cette liste, il faudra rendre votre interface plus lente que celle du modem.  Stockez le débit maximal choisi en kilo-octets par seconde dans les variables shell `DOWNLINK` et `UPLINK` pour le débit descendant et ascendant respectivement, chez moi réglés à 750/75 ko/s pour une connexion de ~800/~100 ko/s.
```bash
tc qdisc add dev $DEV root handle 1:0 htb default 1
tc class add dev $DEV parent 1:0 classid 1:1 htb rate ${UPLINK}kbps
```
Qu’a-t-on fait ?  On a commencé par créer un gestionnaire HTB identifié par le paramètre `handle 1:0` et attaché à la racine de la carte réseau, ce gestionnaire peut avoir plusieurs classes filles, par défaut on envoie tout le flux vers la classe fille 1.  La classe fille est créée en spécifiant le parent et l’identifiant qui prend la forme parent:fille.  Une seule classe fille est créée pour restreindre le débit.  De même pour la connexion descendante :
```bash
tc qdisc add dev $IFB root handle 1:0 htb default 1
tc class add dev $IFB parent 1:0 classid 1:1 htb rate ${DOWNLINK}kbps
```
La limitation artificielle de la bande passante permet de prendre le contrôle sur la gestion des paquets (avant c’était la *box ou le FAI qui l’avait).  Les identifiants doivent être uniques pour une interface réseau donnée, ici par simplicité je m’arrange pour construire des arbres similaires avec des identifiant identiques entre `$DEV` et `$IFB` (ça simplifie un peu les choses).  Il faut bien réaliser que si mon utilisation de HTB est simple il permet beaucoup plus : assurer un débit minimal pour chacune des classes filles et permettre à l’une d’elle d’emprunter à une autre.

prio
-------

On va pouvoir donner des priorités maintenant, il faut utiliser alors le gestionnaire… prio (qui a dit que la QoS était compliquée?).  On commence par le rattacher à la classe qui limite la bande passante, pour le débit ascendant et descendant :
```bash
tc qdisc add dev $DEV parent 1:1 handle 2:0 prio bands 4 priomap 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
tc qdisc add dev $IFB parent 1:1 handle 2:0 prio bands 4 priomap 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
```
J’ai créé un gestionnaire prio qui possède 4 classes filles, paramètre `bands`.  Elles sont automatiquement crées, nommées 2:1, 2:2, etc. par ordre de la priorité la plus élevée à la plus basse.  Le gestionnaire commencera systématiquement par envoyer les paquets de la classe fille 1, si celle-ci est vide il enverra ceux de la fille 2, etc.  La liste des 16 chiffres 3 du paramètre `priomap` définissent la classe fille par défaut vers laquelle envoyer les paquets pour mise en attente.  Il y a un piège : la numérotation commence par 0, donc ici c’est bien vers la quatrième classe, de priorité la plus basse, que j’envoie les paquets par défaut.  Pourquoi réécrire 16 fois la même chose me direz-vous ?  Tout simplement parce que ce gestionnaire tient compte du champ TOS (souvenez-vous je vous en avez parlé à propos de scp et ssh) pour prendre sa décision.  Le champ TOS a 4 bits, soit 16 valeurs possibles… le compte y est.

À propos de SSH, c’est la prochaine étape… je veux qu’il ait la priorité maximale, on va donc filtrer les paquets pour que ceux entrant ou sortant du port 22 de votre serveur SSH soit envoyés dans la classe de plus grande priorité :
```bash
tc filter add dev $IFB parent 2:0 protocol ip prio 1 u32 match ip dport 22 0xffff flowid 2:1
tc filter add dev $DEV parent 2:0 protocol ip prio 1 u32 match ip sport 22 0xffff flowid 2:1
```
Le `parent` définit quel gestionnaire appliquera le filtre et `flowid` vers quelle classe envoyer le paquet.  Le paramètre `prio` définit l’ordre dans lequel sera appliquer les filtres pour un gestionnaire donné.  Mais ceci n’est pas suffisant, on va encore créer un gestionnaire pour les paquets du port 22 :
```bash
tc qdisc add dev $DEV parent 2:1 handle 10:0 prio
tc qdisc add dev $IFB parent 2:1 handle 10:0 prio
```
Le gestionnaire prend l’identifiant 10:0, vous remarquerez que j’utilise toujours prio, mais sans renseigner de paramètres : par défaut 3 classe filles seront créées, et les paquets seront correctement envoyés dans les bonnes classes.  Dans le cas de ssh on arrivera au comportement que j’ai décrit en introduction : les paquets de session intéractive auront la priorité sur les paquets de scp !

SFQ
------

On va finir (enfin!) par le dernier gestionnaire utile : _Stochastic Fairness Queueing_ (SFQ), qui permet d’équilibrer le débit entre les différentes connexions.  Il ne peut pas posséder de classes filles et il est conseillé de terminer l’arbre par le SFQ :
```bash
tc qdisc add dev $IFB parent 10:1 handle 11:0 sfq perturb 10
tc qdisc add dev $IFB parent 10:2 handle 12:0 sfq perturb 10
tc qdisc add dev $IFB parent 10:3 handle 13:0 sfq perturb 10
tc qdisc add dev $DEV parent 10:1 handle 11:0 sfq perturb 10
tc qdisc add dev $DEV parent 10:2 handle 12:0 sfq perturb 10
tc qdisc add dev $DEV parent 10:3 handle 13:0 sfq perturb 10
```

Vérification
========

Pour obtenir les statistiques :
```bash
tc -s class show dev $DEV
tc -s class show dev $IFB
```

TODO : un joli script python tout beau qu’il est bien.

Finalisation
=======

Flux de la *box
-------------------------

Le web
------------

Suppression de la configuration
=====================

```bash
tc qdisc del dev $DEV root
tc qdisc del dev $DEV ingress
tc qdisc del dev $IFB root
rmmod ifb
```

Commentaires
==========

Pour les paquets IPv4 uniquement vous pouvez marquer les paquets avec iptables, et ensuite utiliser les filtres de tc sur les marques.  Ceux qui connaissent iptables pourront trouver cette méthode plus simple et plus puissante que les filtres tc.

*Pour en savoir plus : [Linux Advanced Routing & Traffic Control HOWTO](http://lartc.org), bon courage !*