7. Fonctionnement interne

Ce chapitre délivre des informations, qui ne sont ni nécessaires à la compréhension du fonctionnement du multicast, ni nécessaires à l'écriture de programmes multicast, mais qui deumeurent cependant très intéressantes car elles permettent une compréhension des concepts sur lesquels s'appuient le multicast et ses implémentations. Cela permet ainsi de contourner les erreurs les plus fréquentes et de nombreuses incompréhensions.

Lorsque nous avons parlé de IP_ADD_MEMBERSHIP et de IP_DROP_MEMBERSHIP, nous avions dit que l'information délivrée était utilisée par le noyau pour accepter ou refuser les datagrammes multicast. Cela est vrai mais pas complêtement. Une telle simplification impliquerait que les datagrammes multicast de tous les groupes multicast à travers le monde seraient reçus par notre hôte, et alors il vérifierait les souscriptions des processus s'exécutant sur lui et déciderait s'il délivre l'information ou non. Comme vous pouvez l'imaginer, cela serait du gaspillage de bande passante.

De fait actuellement, un hôte informe son routeur des groupes multicasts qui l'intéressent ; alors, ce routeur informe à son tour les routeurs en amont qu'il souhaite recevoir le trafic en question, et ainsi de suite. Les algorithmes employés servant à demander le trafic d'un groupe donné, ou à l'inverse ceux pour terminer une souscription, peuvent varier. Cependant, il y a quelque chose qui ne change jamais : la manière dont cette information est transmisse. IGMP est utilisé pour cela. IGMP signifie Internet Group Management Protocol. C'est un protocole similaire par de nombreux aspects à ICMP. Il porte un numéro de protocole égal à 2 et ses messages sont transportés dans des datagrammes IP. Tous les hôtes compatibles multicast niveau 2 doivent implémenter ce protocole.

Comme dit précédemment, IGMP est utilisé aussi bien par les hôtes pour donner des informations de souscription à ses routeurs, que par les routeurs pour communiquer entre eux. Par la suite, il ne sera question que des relations entre les hôtes et les routeurs, principalement parce que les informations décrivant les communications entre routeurs (autres que celles provenant du source code de mrouted, RFC-1075) et décrivant le protocole de routage multicast « distance-vecteur » sont maintenant obsolètes. mrouted implémente quant à lui une version de ce protocole non encore documentée.

La version 0 d'IGMP est spécifiée dans le RFC-988 qui est maintenant obsolète. Plus personne n'utilise la version 0 de nos jours.

La version 1 d'IGMP est décrite dans le RFC-1112 et, bien qu'elle soit mise-à-jour par le RFC-2236 (IGMP version 2), elle est encore largement utilisée.

Le noyau Linux implémente complètement IGMP version 1 et en partie la version 2.

Vous trouverez ci-dessous une description informelle du protocole. Vous pouvez consulter le RFC-2236 pour une description formelle, avec beaucoup de diagrammes d'états et d'expiration de délais.

Tous les messages IGMP ont la structure suivante :

0

1

2

3

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

Type Temp max. de réponse contrôle d'intégrité
Adresse de groupe

Dans le cas d'IGMP version 1 (appelé ci-après IGMPv1) le champ « temp maximum de réponse » est marqué comme « inutilisé ». Il est rempli de zéros lors d'une émission et est ignoré à la réception. De plus, cette version d'IGMP coupe le champ « type » en deux sous champs de longueur 4 bits : « version » et « type ». Comme IGMPv1 identifie un message d'interrogation de souscription comme étant 0x11 (version 1, type 1) et IGMPv2 aussi, les 8 bits ont la même signification.

Je pense qu'il est plus instructif de donner d'abord une description d'IGMPv1 et de pointer ensuite les additions faites par IGMPv2.

À la lecture de la description faite ci-dessous, gardez à l'esprit qu'un routeur multicast reçoit tous les datagrammes IP multicast.

Les routeurs émettent périodiquement des requêtes IGMP sur les souscriptions des hôtes (IGMP Host Membership Queries). Ces requêtes sont faites sur le groupe all-hosts (224.0.0.1) avec un TTL de 1, ceci toutes les 1 ou 2 minutes. Tous les hôtes supportant le multicast entendent cela, mais ne répondent pas immédiatement pour éviter une tempête de réponses. Au lieu de cela, ils déclenchent un compte à rebours pour chaque groupe auquel ils appartiennent, et cela sur l'interface sur laquelle ils ont reçu la demande.

Tôt ou tard, le compte à rebours expirera sur un des hôtes, ce dernier envoiera alors un IGMP Host Membership Report (résultat d'appartenance) avec un TTL de 1 à l'adresse multicast du groupe devant être rapporté. Comme le résultat est envoyé au groupe, tous les hôtes qui ont rejoint ce dernier -et qui sont actuellement en train d'attendre que leur propre délai expire- le recoive aussi. Alors, ces hôtes stoppent leur compte à rebours, et ne génèrent pas d'autres rapports. Un seul rapport est généré - par l'hôte ayant le délai le plus court-, et cela est suffisant pour le routeur. Celui-ci doit seulement savoir s'il y a des membres de ce groupe dans le sous réseau, pas qui et combien ils sont.

Lorsqu'aucune réponse n'est reçue pour un groupe donné après un certain nombre de requêtes, le routeur déduit qu'il n'y a pas de membre de ce groupe sur ce sous réseau, et de ce fait n'a pas à transmettre le trafic. Notez que pour IGMPv1 il n'y a pas de message de fin d'appartenance de groupe.

Lorsqu'un hôte rejoint un nouveau groupe, le noyau envoie un rapport concernant ce groupe, ainsi il n'est pas nécessaire d'attendre une ou deux minutes jusqu'a ce que la demande d'appartenance suivante soit formulée. Comme vous pouvez le constater le paquet IGMP est généré par le noyau comme étant une réponse à la commande IP_ADD_MEMBERSHIP, comme vu dans la section IP_ADD_MEMBERSHIP. Notez l'importance de l'adjectif nouveau : si la commande IP_ADD_MEMBERSHIP est effectuée sur un hôte déjà membre de ce groupe, aucun paquet IGMP n'est envoyé car nous recevons déjà le trafic pour ce groupe ; à la place, un compteur sur le nombre d'utilisateurs de ce groupe est incrémenté. IP_DROP_MEMBERSHIP ne génère aucun datagramme dans IGMPv1.

Les requêtes sur les souscriptions des hôtes sont identifiées par le type 0x11, et les rapports d'appartenance par le type 0x12.

Aucun rapport n'est envoyé pour le groupe all-hosts. L'appartenance à ce dernier groupe est permanente.

L'ajout majeur dans cette version du protocole est l'inclusion de messages de fin d'appartenance de groupe (Leave Group message) noté 0x17. La raison de cet ajout est d'éviter un gaspillage de bande passante entre le moment où le dernier hôte du sous-réseau quitte ses souscriptions et le moment où le routeur s'aperçoit qu'il n'y a plus de membre de ce groupe présent dans ce sous réseau (leave latency). Les messages de fin d'appartenance doivent être adressés au groupe all-routers (224.0.0.2) plus qu'au groupe que l'on vient de quitter, car l'information n'a pas d'utilité pour les autres membres de ce groupe (les versions du noyau ultérieures à 2.0.33 envoyent l'information au groupe ; et bien que cela ne porte pas préjudice aux hôtes, cela constitue une perte de temps car ces messages doivent êtres traités, sans pour autant donner d'information utile). Il est à noter qu'il existe certains détails subtiles à propos du moment où il est approprié ou non d'envoyer des Messages de fin d'appartenance ; si cela vous intéresse, reportez-vous au RFC.

Quant un router IGMPv2 reçoit un message de fin d'appartenance de groupe, il envoie une requête « spécifique de groupe » (Group-Specific Queries) au groupe à quitter. Cela contitue un autre ajout. IGMPv1 n'a pas ces requêtes spécifiques de groupes. Toutes ces requêtes sont envoyées au groupe all-hosts. Le champ type de l'entête IGMP n'est pas amené à changer (0x11 comme précédemment), mais le champ « adresse du groupe » est rempli avec l'adresse du groupe multicast à quitter.

Le champ Temps Maximum de Réponse, qui dans IGMPv1 était mis à 0 pour la transmission et ignoré à la réception, devient significatif seulement dans les messages de requêtes en appartenance (ou Membership Query). Ce champ informe du temps maximum alloué en 1/10 de secondes avant l'envoi d'un rapport. Il est donc utilisé comme mécanisme d'écoute.

IGMPv2 introduit un nouveau type de message : 0x16. Il s'agit d'un « rapport d'appartence IGMPv2 » envoyé par les hôtes IGMPv2 si ceux-ci détectent qu'un routeur IGMPv2 est présent (un hôte IGMPv2 sait qu'un routeur IGMPv1 est présent s'il reçoit une requête avec le champ « Réponse Max » à 0).

Lorsque plus d'un routeur se réclame comme agissant en interrogateur, IGMPv2 fournit un mécanisme pour écourter les discussions : le routeur possédant la plus petite adresse IP est désigné comme étant l'interrogateur. Les autres routeurs restent à l'écoute d'éventuels dépassements de temps de réponse. Ainsi, si le routeur ayant la plus petite adresse IP « tombe » ou est éteint, la décision pour connaitre le nouvel interrogateur est prise après que les délais aient expiré.

Cette sous-section donne le point de départ de l'étude de l'implémentation du multicast dans le noyau Linux. Cela n'explique pas l'implémentation faite. Elle indique juste où trouver les éléments.

L'étude a été menée sur la version 2.0.32, de ce fait il se peut qu'elle soit dépassée au moment où vous allez la lire (le code réseau du noyau semble avoir beaucoup changé dans les versions 2.1.x).

Le code multicast dans les noyaux Linux est toujours entouré par une paire de balises #ifdef CONFIG_IP_MULTICAST / #endif, de cette façon vous pouvez l'inclure ou l'exclure de votre noyau selon vos besoins (cette inclusion/exclusion est faite à l'étape de compilation, comme vous le savez probablement… Les #ifdefs sont interprétés par le pré-processeur. La décision est prise selon les choix que vous avez effectués lors du make config, make menuconfig ou make xconfig).

Vous désirez probablement activer les fonctionnalités multicast, mais si votre machine Linux n'a pas à se comporter comme un routeur multicast, vous n'aurez sûrement pas besoin d'activer le routage multicast du noyau. Le code concernant le routage multicast est contenu entre la paire de balises #ifdef CONFIG_IP_MROUTE / #endif.

Les sources du noyau sont couramment placées dans le répertoire /usr/src/linux. Cependant, l'emplacement peut changer, de ce fait l'emplacement du répertoire de base des sources du noyau sera designé ici comme étant LINUX. De ce fait, LINUX/net/ipv4/udp.c correspond à /usr/src/linux/net/ipv4/udp.c si vous avez extrait les sources du noyau dans le répertoire /usr/src/linux.

Toutes les interfaces multicast désignées dans cette section et dédiées à la programmation d'applications multicast, s'utilisent au travers des appels systèmes setsockopt() / getsockopt().

L'un comme l'autre est implémenté au moyen de fonctions qui effectuent quelques tests pour vérifier les paramètres qui leurs sont passés et qui, à tour de rôle, appellent une autre fonction qui effectue quelques tests complémentaires, démultiplexant l'appel passé dans le paramètre level pour chaque appel système, et appelle alors une autre fonction qui… (si vous êtes intéressé par tous ces sauts, vous pouvez les suivre dans LINUX/net/socket.c (fonctions sys_socketcall() et sys_setsockopt()), LINUX/net/ipv4/af_inet.c (fonction inet_setsockopt()) et LINUX/net/ipv4/ip_sockglue.c (fonction ip_setsockopt())).

Le fichier qui nous intéresse ici est LINUX/net/ipv4/ip_sockglue.c. Nous y trouvons ip_setsockopt() et ip_getsockopt() qui sont essentiellement des switchs (après quelques vérifications d'erreurs) vérifiant chaque valeur possible pour optname. Tout comme les options unicast, les options multicast sont capturées : IP_MULTICAST_TTL, IP_MULTICAST_LOOP, IP_MULTICAST_IF, IP_ADD_MEMBERSHIP et IP_DROP_MEMBERSHIP. Avant le switch, un test est fait pour savoir si les options sont spécifiques au routeur multicast, et dans ce cas, elles sont redirigées vers les fonctions ip_mroute_setsockopt() et ip_mroute_getsockopt() (fichier LINUX/net/ipv4/ipmr.c).

Dans le fichier LINUX/net/ipv4/af_inet.c nous pouvons voir les valeurs par défaut -dont nous parlions dans les sections précédentes (loopback activé, TTL=1)- données à la prise réseau lorsqu'elle est crée (prises depuis la fonction inet_create() de ce fichier) :

#ifdef CONFIG_IP_MULTICAST
  sk->ip_mc_loop=1;
  sk->ip_mc_ttl=1;
 *sk->ip_mc_name=0;
  sk->ip_mc_list=NULL;
#endif

L'assertion stipulant que « la fermeture d'une prise réseau implique que le noyau n'accepte plus les souscriptions faites par cette prise » est corroborée par :

#ifdef CONFIG_IP_MULTICAST
/* Les applications oublient de quitter les groupes avant de partir */
  ip_mc_drop_socket(sk);
#endif

Extrait de la fonction inet_release(), dans le même fichier que précédemment.

Les opérations indépendantes de la couche de liaison et relatives aux périphériques sont gardées dans le fichier LINUX/net/core/dev_mcast.c.

Deux fonctions sont encore manquantes : les fonctions concernant le traîtement des paquets multicast en entrée et en sortie. Comme tous les autres paquets, les paquets entrants sont transmis depuis les pilotes de périphériques à la fonction ip_rcv() (LINUX/net/ipv4/ip_input.c). Cette fonction contient le filtrage parfait appliqué aux paquets multicasts qui ont passé la couche du périphérique (souvenez-vous que les couches les plus basses ne fournissent qu'un filtrage approximatif et ce n'est que la couche IP qui sait à 100% si nous sommes intéressés ou non par ce groupe multicast). Si l'hôte fonctionne comme un routeur multicast, alors la fonction décide aussi si le paquet doit être transmis. Il appelle alors ipmr_forward(). Cette dernière fonction est implémentée dans le fichier LINUX/net/ipv4/ipmr.c.

Le code se chargeant des paquets émis en sortie est contenu dans le fichier LINUX/net/ipv4/ip_output.c. On y trouve l'effet possible de l'option des IP_MULTICAST_LOOP, selon que l'on souhaite ou non faire une boucle locale (fonction ip_queue_xmit()). Ainsi la valeur du TTL du paquet sortant est donné selon qu'il s'agit d'un paquet unicast ou multicast. Dans ce dernier cas, la valeur de l'argument IP_MULTICAST_TTL passé en option est utilisée (fonction ip_build_xmit()).

Durant un travail à l'aide de mrouted (un programme fournissant des informations liées au noyau sur la façon de router des datagrammes multicast), nous avons détecté que les paquets multicast provenant du réseau local sont routés correctement exceptés ceux venant de la machine Linux agissant en tant que routeur multicast ! ip_input.c fonctionne bien, mais il semble que ce n'est pas le cas de ip_output.c. En lisant le code source des fonctions de sorties, nous avons trouvé que les paquets sortant n'étaient pas passés à ipmr_forward(), fonction décidant si les paquets doivent être routés ou non. Les paquets sont sortis sur le réseau local mais, comme les cartes réseaux sont le plus souvent incapables de lire les données transmises, ces datagrammes ne sont alors jamais routés. Nous avons ajouté le code nécessaire à la fonction ip_build_xmit() et le tour était joué. (Disposer du code source du noyau n'est pas un luxe, mais une nécessité !)

ipmr_forward() a déjà été mentionné un certain nombre de fois. C'est une fonction importante car elle permet de résoudre un important problème de compréhension nécessitant d'être plus largement expliqué. En routant du trafic multicast, ce n'est pas mrouted qui fabrique les copies puis les expédie à ses propres destinataires. mrouted reçoit tout le trafic multicast et, basé sur cette information, calcule les tables de routages multicast puis informe le noyau de la manière de router : « les datagrammes pour ce groupe provenant de cette interface doivent être transférés à ces interfaces ». Cette information est transmise au noyau par l'appel de setsockopt() sur la file d'une prise réseau ouverte par le daemon mrouted (le protocole spécifié lors de la création de la file de la prise réseau doit être IPPROTO_IGMP). La prise en compte d'options s'effectue par l'appel à la fonction ip_mroute_setsockopt() de LINUX/net/ipv4/ipmr.c. Cette première option (il est préférable de les appeler commandes plutôt qu'options) provenant de cette prise réseau doit être MRT_INIT. Toutes les autres commandes sont ignorées (retournant -EACCES) si MRT_INIT n'est pas précisé en premier. Seulement une seule instance de mrouted peut être exécutée à la fois sur un même hôte. Pour garder une trace de cela, lorsque le premier MRT_INIT est reçu, une variable importante, struct sock* mroute_socket, est pointée sur la prise réseau ou le MRT_INIT a été reçu. Si mroute_socket n'est pas nul lors de l'attente d'un MRT_INIT cela signifie qu'un autre mrouted est en cours d'exécution, un -EADDRINUSE est alors renvoyé. Toutes les commandes s'appuyant sur le même principe (MRT_DONE, MRT_ADD_VIF, MRT_DEL_VIF, MRT_ADD_MFC, MRT_DEL_MFC et MRT_ASSERT) retournent -EACCES si elles proviennent d'une prise réseau différente de mroute_socket.

Comme les datagrammes multicast routés peuvent être reçus/envoyés au travers d'interfaces physiques ou de tunnels, une abstraction commune a été définie : les VIFs, InterFaces Virtuelles (ou Virtual InterFaces). mrouted communique les structures VIFs au noyau, en indiquant les interfaces physiques ou tunnels pour qu'il les ajoute à sa table de routage. Les entrées de retransmissions multicast indiquent où transmettre les datagrammes.

Les VIFs sont ajoutées à l'aide de MRT_ADD_VIF et supprimées avec MRT_DEL_VIF. L'un comme l'autre passent un struct vifctl au noyau (défini dans /usr/include/linux/mroute.h) avec les informations suivantes :

struct vifctl {
  vifi_t  vifc_vifi;             /* Index du VIF */
  unsigned char vifc_flags;      /* Attributs VIFF_ */
  unsigned char vifc_threshold;  /* Seuil du ttl */
  unsigned int vifc_rate_limit;  /* Valeur de débit limite : non implémenté */
  struct in_addr vifc_lcl_addr;  /* Notre adresse */
  struct in_addr vifc_rmt_addr;  /* Adresse du tunnel IPIP */
};

Avec cette information une structure vif_device est construite :

struct vif_device
{
  struct device   *dev;                   /* le périphérique que nous utilisons */
  struct route    *rt_cache;              /* Tunnel route cache */
  unsigned long   bytes_in,bytes_out;
  unsigned long   pkt_in,pkt_out;         /* Statistiques */
  unsigned long   rate_limit;             /* Forme du trafic ; non implémenté */
  unsigned char   threshold;              /* seuil du TTL */
  unsigned short  flags;                  /* attributs de contrôle */
  unsigned long   local,remote;           /* Adresses(distantes pour les tunnels)*/
};

Notez l'entrée dev dans la structure. La structure device est définie dans le fichier /usr/include/linux/netdevice.h. C'est une grosse structure, mais le champ qui nous intéresse est :

struct ip_mc_list*    ip_mc_list;   /* chaîne des filtres IP multicast */

La structure ip_mc_list -définie dans /usr/include/linux/igmp.h- est comme suit :

struct ip_mc_list
{
  struct device *interface;
  unsigned long multiaddr;
  struct ip_mc_list *next;
  struct timer_list timer;
  short tm_running;
  short reporter;
  int users;
};

Ainsi, le membre ip_mc_list de la structure dev est un pointeur sur une liste chaînée de structures ip_mc_list, chacune contenant une entrée pour chaque groupe multicast pour laquelle l'interface est membre. Ici nous voyons une fois de plus que les appartenances sont associées à des interfaces. LINUX/net/ipv4/ip_input.c parcourt cette liste chaînée pour décider si le datagramme reçu est destiné à un groupe auquel l'interface appartient :

#ifdef CONFIG_IP_MULTICAST
  if(!(dev->flags&IFF_ALLMULTI) && brd==IS_MULTICAST
     && iph->daddr!=IGMP_ALL_HOSTS
     && !(dev->flags&IFF_LOOPBACK))
  {
    /*
     * Verification de l'appartenance à un de nos groupes
     */
     struct ip_mc_list *ip_mc=dev->ip_mc_list;
     do
     {
       if(ip_mc==NULL)
       {
         kfree_skb(skb, FREE_WRITE);
         return 0;
       }
       if(ip_mc->multiaddr==iph->daddr)
         break;
       ip_mc=ip_mc->next;
     }
     while(1);
  }
#endif

Le champ users de la strucuture ip_mc_list est utilisé pour implémenter ce que nous avons dit dans la section IGMP version 1 : si un processus joint un groupe et que l'interface est déjà membre de ce groupe (ie, un autre processus joint le même groupe sur la même interface que précédemment) seul le compteur du nombre de membres (users) est incrémenté. Aucun message IGMP n'est envoyé, comme vous pouvez le voir dans le code suivant (extrait de ip_mc_inc_group(), appelé par ip_mc_join_group(), provenant du fichier LINUX/net/ipv4/igmp.c) :

for(i=dev->ip_mc_list;i!=NULL;i=i->next)
  {
    if(i->multiaddr==addr)
    {
      i->users++;
      return;
    }
  }

À chaque suppression d'une souscription de groupe, le compteur est décrémenté. Des opérations supplémentaires sont appliquées seulement si le compteur est devient égal à 0 (fonction ip_mc_dec_group()).

MRT_ADD_MFC et MRT_DEL_MFC ajoutent ou suppriment des entrées de retransmission dans la table de routage multicast. L'un comme l'autre passent un struct mfcctl au noyau (défini dans /usr/include/linux/mroute.h) avec cette information :

struct mfcctl
{
  struct in_addr mfcc_origin;       /* Origine du mcast      */
  struct in_addr mfcc_mcastgrp;     /* Groupe en question    */
  vifi_t  mfcc_parent;              /* Où cela arrive     */
  unsigned char mfcc_ttls[MAXVIFS]; /* Où cela va t-il    */
};

Avec toute cette information en main, ipmr_forward() parcourt les VIFs, et si une correspondance est trouvée, on duplique le datagramme et on appelle ipmr_queue_xmit() qui, à tour de rôle, utilise le périphérique de sortie indiqué par la table de routage et une adresse de destination propre si le paquet est à envoyer au travers d'un tunnel (il s'agit en fait de l'adresse de destination unicast de l'autre coté du tunnel).

La fonction ip_rt_event() (qui n'est pas directement apparentée à la sortie des datagrammes, mais qui se trouve pourtant dans ip_output.c) reçoit les évènements relatifs aux périphériques réseaux, tel que le branchement du périphérique par exemple. Cette fonction assure que le périphérique joint bien le groupe multicast ALL-HOSTS.

Les fonctions IGMP sont implémentées dans le fichier LINUX/net/ipv4/igmp.c. Les informations importantes concernant ces fonctions se trouvent dans /usr/include/linux/igmp.h et /usr/include/linux/mroute.h. L'entrée IGMP dans le répertoire /proc/net est créée à l'aide de la fonction ip_init() contenue dans le fichier LINUX/net/ipv4/ip_output.c.