Programmation d'applications bis

From Livre IPv6

Revision as of 14:42, 2 July 2009 by Eduble (Talk | contribs)

Résumé

L'évolution de IPv4 vers IPv6 a été conçue pour minimiser les changements visibles. Un grand nombre de concepts n'ont pas changé : les noms, les "ports", l'envoi et la réception de données,... Un certain nombre de points ont malgré tout dû être modifiés. Le principal est lié à la taille de l'adresse : en IPv4, une adresse a une longueur de 32 bits (et de nombreux programmes confondent les types adresse et entier) alors qu'en IPv6 une adresse a une longueur de 128 bits ; les types liés aux adresses doivent donc être modifiés. En fait l'effet est plus profond : les nouvelles structures sont plus grandes, et certaines réservations de mémoire avec conversion de type implicite (en particulier : un entier pour une adresse, une struct sockaddr pour une struct sockaddr_in, un tampon de 16 octets pour afficher une adresse sous forme numérique) doivent être corrigés sous peine de débordement de mémoire.

L'interface de programmation réseau ("API") la plus connue est l'interface "socket" (dite aussi interface "BSD"). Le but de ce chapitre est de présenter pour cette interface de programmation les modifications introduites pour supporter IPv6, et notamment de donner une brève description des nouvelles primitives d'appel au DNS et de conversion d'adresses.

Ces modifications ont été définies pour être aussi transparentes que possible, et, s'il est en pratique toujours nécessaire de modifier un programme pour le porter de IPv4 à IPv6, un programme conçu avec des règles de typage strict est portable sans grandes modifications.

Ce chapitre illustrera l'interface de programmation "socket" pour IPv6 en présentant plusieurs exemples de programmes. Plus précisément, il détaillera successivement :

  • un programme combinant les différentes fonctions de conversion d'adresse ;
  • un client/serveur TCP calculant le nombre d'utilisateurs connectés sur une machine cible. En particulier, on aura soin de comparer les codes IPv4 et IPv6 de ce client/serveur, ce qui amènera à constater qu'à ce niveau de programmation, la migration vers IPv6 n'offre aucune difficulté ;
  • un "mini ping" qui permettra de se familiariser avec le protocole ICMPv6 qui présente de notables différences avec son prédécesseur le protocole ICMPv4 ;
  • un exemple qui génère un trafic multicast, avec abonnement et désabonnement ;
  • un programme illustant l'utilisation de l'API socket avancée.


Ce qui a changé dans l'API

Généralités

Des changements ont été opérés dans l'API de façon à intégrer IPv6.

Ces changements ont été minimisés autant que possible de manière à faciliter le portage des applications IPv4 existantes. En outre, et ce point est important, cette nouvelle API doit permettre l'interopérabilité entre machines IPv4 et machines IPv6 grâce au mécanisme de double pile décrit ci-après.

L'API décrite ici est celle utilisée en Solaris, Linux et systèmes *BSD. Elle correspond à celle définie dans le RFC 3493 avec quelques modifications nécessaires pour prendre en compte les dernières évolutions des protocoles sous-jacents. Cette API est explicitement conçue pour fonctionner sur des machines possédant la double pile IPv4 et IPv6. Cette API "socket" est celle disponible dans de nombreux environnements de programmation tels que Java, perl, python, ruby, ...

Une API "avancée", décrite dans le RFC 3542 permet de programmer les échanges réseaux de manière très précise. Elle sera également présentée mais de manière succinte et essentiellement par le biais de l'exemple one_ping6.

Changements à bas niveau

Cette partie résume les changements effectués dans l'API à bas niveau. Il est important de noter qu'en cas de programmation indépendante de la famille d'adresses (ce qui est conseillé), ces nouvelles constantes, structures ou fonctions n'apparaitront pas ou peu dans le code.

Apparition des constantes PF_INET6 et AF_INET6

Une nouvelle famille d'adresses ayant pour nom AF_INET6 et dont la valeur peut varier d'une implémentation à l'autre, a été définie (dans sys/socket.h). Également, une nouvelle famille de protocoles ayant pour nom PF_INET6 a été définie (dans sys/socket.h). En principe, on doit avoir :

#define PF_INET6 AF_INET6


Apparition de nouvelles structures

La structure de données destinée à contenir une adresse IPv6 est définie comme suit (dans netinet/in.h) :

struct in6_addr {
   uint8_t s6_addr[16];
};

les octets constituant l'adresse étant rangés comme d'habitude dans l'ordre réseau (network byte order).

La structure de données IPv6 struct sockaddr_in6, est équivalente à la structure struct sockaddr_in d'IPv4. Elle est définie comme suit (dans netinet/in.h) pour les systèmes dérivés d'UNIX 4.3BSD :

struct sockaddr_in6 {
   sa_family_t sin6_family;   /* AF_INET6 */
   in_port_t sin6_port;       /* numéro de port */
   uint32_t sin6_flowinfo;    /* identificateur de flux */
   struct in6_addr sin6_addr; /* adresse IPv6 */
   uint32_t sin6_scope_id;    /* ensemble d'interfaces correspondant
                               * à la portée de l'adresse */
};

Il faut noter que cette structure a une longueur de 28 octets, et est donc plus grande que le type générique struct sockaddr. Il n'est donc plus possible de réserver une struct sockaddr si la valeur à stocker peut être une struct sockaddr_in6. Afin de faciliter la tâche des implémenteurs, une nouvelle structure de données, struct sockaddr_storage, a été définie. Celle-ci est de taille suffisante afin de pouvoir prendre en compte tous les protocoles supportés et alignée de telle sorte que les conversions de type entre pointeurs vers les structures de données d'adresse des protocoles supportés et pointeurs vers elle-même n'engendrent pas de problèmes d'alignement. Un exemple d'utilisation pourrait être le suivant :

struct sockaddr_storage ss;
 
struct sockaddr_in *sin = (struct sockaddr_in *) &ss;
struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) &ss;

Dans la version 4.4 d'UNIX BSD, la longueur du champ sin6_family est passée de 2 octets à 1 octet. L'octet ainsi récupéré contient la taille de la structure sockaddr_in6 et sert à effectuer correctement la conversion de type vers la structure de données générique sockaddr utilisée par bon nombre de primitives de l'interface socket.

La macro-définition SIN6_LEN, présente dans toute implémentation 4.4BSD, permet alors de distinguer les versions. Les autres champs restant inchangés, cette structure est presque identique à celle de la précédente version :

#define SIN6_LEN
 
struct sockaddr_in6 {
   u_int8_t sin6_len;         /* la longueur de cette structure */
   sa_family_t sin6_family;   /* AF_INET6 */
   in_port_t sin6_port;       /* numéro de port */
   uint32_t sin6_flowinfo;    /* identificateur de flux */
   struct in6_addr sin6_addr; /* adresse IPv6 */
   uint32_t sin6_scope_id;    /* ensemble d'interfaces correspondant
                               * à la portée de l'adresse */
};

Si le champ sin6_len existe (ce qui est testable par le fait que le symbole SIN6_LEN est défini), il doit être initialisé par la taille de la structure sockaddr_in6.

On notera la présence de deux nouveaux champs (ils n'ont pas d'équivalents dans la structure sockaddr_in) dans la structure de données sockaddr_in6, les champs sin6_flowinfo et sin6_scope_id. Le premier, en réalité structuré, est décrit dans le RFC 2460 et Identificateur de flux. Le second désigne un ensemble d'interfaces en adéquation avec la portée de l'adresse contenue dans le champ sin6_addr. Par exemple, si l'adresse en question est de type lien local, le champ sin6_scope_id devrait être un index d'interface.

L'adresse "wildcard"

Lors du nommage d'une socket via la primitive bind, il arrive fréquemment qu'une application (par exemple un serveur TCP) laisse au système la détermination de l'adresse source pour elle. En IPv4, pour ce faire, elle passe à bind une structure sockaddr_in avec le champ sin_addr.s_addr ayant pour valeur la constante INADDR_ANY, constante définie dans le fichier netinet/in.h.

En IPv6, il y a deux manières de faire cela, à cause des règles du langage C sur les initialisations et affectations de structures. La première est d'initialiser une structure de type struct in6_addr par la constante IN6ADDR_ANY_INIT :

struct in6_addr any_addr = IN6ADDR_ANY_INIT;

Attention, ceci ne peut se faire qu'au moment de la déclaration. Par exemple le code qui suit est incorrect (en C il est interdit d'affecter une constante complexe à une structure) :

struct sockaddr_in6 sin6;
 
sin6.sin6_addr = IN6ADDR_ANY_INIT; /* erreur de syntaxe !! */

La seconde manière utilise une variable globale :

extern const struct in6_addr in6addr_any;
struct sockaddr_in6 sin6;
 
sin6.sin6_addr = in6addr_any;

Cette méthode n'est pas possible dans une déclaration de variable globale ou statique.

La constante IN6ADDR_ANY_INIT et la variable in6addr_any sont toutes deux définies dans le fichier netinet/in.h.

L'adresse de bouclage

En IPv4, c'est la constante INADDR_LOOPBACK. En IPv6, de manière tout à fait similaire à l'adresse "wildcard", il y a deux façons d'affecter cette adresse. Ceci peut se faire au moment de la déclaration avec la constante IN6ADDR_LOOPBACK_INIT :

struct in6_addr loopback_addr = IN6ADDR_LOOPBACK_INIT;

ou via la variable globale in6addr_loopback :

extern const struct in6_addr in6addr_loopback;
struct sockaddr_in6 sin6;
sin6.sin6_addr = in6addr_loopback;

Cette constante et cette variable sont définies dans le fichier netinet/in.h.


Apparition de fonctions de conversion

Apparition de fonctions de conversion entre chaine de caractères représentant une adresse IP et son format binaire

Changements impliquant souvent un travail de migration

Deux de ces changements impliquent souvent un travail de migration important :

  • Introduction de nouvelles primitives pour la résolution de noms et la résolution inverse. La nouvelle primitive de résolution de noms renvoie une liste d'adresses (IPv4 et IPv6), alors que la primitive gethostbyname() ne renvoyait qu'une seule adresse. Cela implique une adaptation des programmes en conséquence.
  • Les adresses IPv6 sont plus longues que les adresses IPv4, il faut donc adapter les structures où elles sont stockées. En particulier, la structure struct sockaddr_storage a été créée. Elle peut contenir une adresse IP quel que soit le protocole.


Programmation sur un système double-pile

Les système d'exploitation actuels implémentent une double-pile IPv4 / IPv6. Cette partie détaille les possibilités offertes par cette architecture.

Compatibilité IPV4 et IPv6

La transition entre les protocoles IPv4 et IPv6 étant étendue dans le temps, le but n'est pas d'implémenter ou migrer des programmes vers IPv6 en oubliant le protocole IPv4. Ces programmes doivent gérer les deux protocoles.

Programmation d'un serveur

Deux types de serveurs

Il existe deux méthodes pour programmer un serveur acceptant à la fois des clients IPv4 et IPv6.

La première solution est d'ouvrir une socket PF_INET et une socket PF_INET6, gérant respectivement le traffic IPv4 (en orange sur la figure ci-contre) et IPv6 (en bleu). Le serveur doit ensuite écouter simultanément sur ces deux sockets (utilisation d'un select()). Serveur utilisant une socket PF_INET et une socket PF_INET6
Figure : Serveur utilisant une socket PF_INET et une socket PF_INET6
La deuxième solution est d'ouvrir une seule socket PF_INET6, qui acceptera à la fois les clients IPv4 et IPv6. Dans ce cas les adresses IPv4 sont traduites en adresses "IPv4 mappées en IPv6" (traffic jaune sur la figure ci-contre) de façon à ce que le traffic IPv4 puisse atteindre la socket PF_INET6. Cette traduction est faite par le système d'exploitation, et activée ou désactivée suivant la valeur de l'option de socket IPV6_V6ONLY. Serveur utilisant une seule socket PF_INET6
Figure : Serveur utilisant une seule socket PF_INET6

L'option IPV6_V6ONLY

L'option IPV6_V6ONLY contrôle si la socket serveur PF_INET6 doit gérer uniquement le traffic IPv6 ou bien si elle doit gérer à la fois le traffic IPv6 et le traffic IPv4 (mappé).

Sa valeur doit être définie en utilisant setsockopt(). Typiquement :

setsockopt(sockfd, IPPROTO_IPV6, IPV6_V6ONLY, &optval, sizeof(optval));

Voici les cas à prévoir en cas d'option mal positionnée :

  • Un serveur conçu avec deux sockets qui aurait IPV6_V6ONLY à 0 échouerait à ouvrir la deuxième socket (erreur EADDRINUSE).
  • Un serveur conçu avec une seule socket et qui aurait IPV6_V6ONLY à 1 ignorerait le traffic IPv4.

Si cette option n'est pas définie, la valeur par défaut dépend du système d'exploitation sur lequel le serveur est exécuté. Par exemple, sur Linux/*BSD la valeur par défaut est donnée (et modifiable) par sysctl net.ipv6.bindv6only. Ceci donne 0 par défaut sous Linux, et 1 sur *BSD...

En conséquence, un programme qui se veut portable doit explicitement donner une valeur à cette option.

L'interface de programmation "socket" IPv6

L'interface socket

La création d'une socket se fait comme auparavant en appelant la primitive socket. La distinction entre les protocoles IPv4 et IPv6 se fait sur la valeur du premier argument passé à socket, à savoir la famille d'adresses (ou de protocoles), c'est-à-dire ici PF_INET ou PF_INET6. Par exemple, si on veut créer un socket IPv4/UDP, on écrira :

sock = socket(PF_INET, SOCK_DGRAM, 0);

tandis qu'une création de socket IPv6/UDP se fera ainsi :

sock = socket(PF_INET6, SOCK_DGRAM, 0);

Une erreur de programmation classique consiste à utiliser AF_INET à la place de PF_INET. Cela n'a pas d'effet en général car rares sont les systèmes pour lesquels ces deux constantes diffèrent. Pour éviter en IPv6 des problèmes liés à cette erreur, il est demandé que les deux constantes PF_INET6 et AF_INET6 soient identiques.

Quant aux autres primitives constituant l'interface socket, leur syntaxe reste inchangée. Il faut simplement leur fournir des adresses IPv6, en l'occurrence des pointeurs vers des structures de type struct sockaddr_in6 au préalable convertis en des pointeurs vers des structures génériques de type struct sockaddr.

Donnons pour mémoire une liste des primitives les plus importantes :

bind()    connect()     sendmsg()
sendto()  accept()      recvfrom()
recvmsg() getsockname() getpeername()


Les primitives de conversion entre noms et adresses

Les primitives gethostbyname, gethostbyaddr, getservbyname et getservbyport ont été remplacées par les deux primitives indépendantes de la famille d'adresses et normalisées par la RFC 3493 getaddrinfo et getnameinfo :

#include <sys/socket.h>
#include <netdb.h>
 
int
getaddrinfo(const char *nodename, const char *servname,
const struct addrinfo *hints, struct addrinfo **res);
 
void freeaddrinfo(struct addrinfo *res);
 
const char *gai_strerror(int errcode);

Le type struct addrinfo est défini comme suit :

struct addrinfo {
   int ai_flags;             /* AI_PASSIVE, AI_CANONNAME, ... */
   int ai_family;            /* PF_xxx */
   int ai_socktype;          /* SOCK_xxx */
   int ai_protocol;          /* 0 ou IPPROTO_xxx pour IPv4 et IPv6 */
   size_t ai_addrlen;        /* la taille de l'adresse binaire ai_addr */
   char *ai_canonname;       /* le nom complétement qualifié */
   struct sockaddr *ai_addr; /* l'adresse binaire */
   struct addrinfo *ai_next; /* la structure suivante dans la liste chaînée */
};

getaddrinfo prend en entrée le nom d'une machine (nodename) et le nom d'un service (servname). S'il n'y a pas d'erreur, getaddrinfo rend 0 et res pointe sur une liste dynamiquement allouée de struct addrinfo. Chaque élément de cette liste contient la description et l'adresse d'une struct sockaddr initialisée pour fournir l'accès au service servname sur nodename. Les champs ai_family, ai_socktype et ai_protocol ont la valeur utilisable dans l'appel système socket.

Lorsque la liste de résultat n'est plus nécessaire, la mémoire allouée peut être libérée par la primitive freeaddrinfo. En cas d'erreur, getaddrinfo rend un code d'erreur non nul qui peut être imprimé par la fonction gai_strerror.

getaddrinfo peut donner des réponses de la famille d'adresses IPv4 ou IPv6, et des réponses pour les protocoles connectés ou non (ai_socktype peut valoir SOCK_DGRAM ou SOCK_STREAM). L'argument hints permet de choisir les réponses souhaitées. Un argument égal à NULL signifie que la liste des réponses doit contenir toutes les adresses et tous les protocoles. Sinon hints doit pointer sur une structure dont les champs ai_family, ai_socktype et ai_protocol définissent les types de résultat attendus. Une valeur de PF_UNSPEC du champ ai_family signifie que toutes les familles d'adresse (IPv4 et IPv6) sont admises, un 0 dans les champs ai_socktype (resp. ai_protocol) signifie que tous les types de socket (resp. protocole) sont admis. Le champ ai_flags permet de préciser des options supplémentaires.

L'argument servname peut être le nom d'un service ou un nombre décimal. De même, l'argument nodename peut être un nom (au format DNS habituel) ou une adresse sous forme numérique IPv4 ou IPv6 (si ai_flags contient le bit AI_NUMERICHOST, nodename doit être sous forme numérique et aucun appel au serveur de nom n'est fait). De plus l'un ou l'autre des arguments servname et nodename peut être un pointeur NULL, mais pas tous les deux. Si servname est NULL, le champ port des réponses ne sera pas initialisé (il restera égal à 0). Si nodename est NULL, l'adresse réseau dans les réponses est mis à "non initialisé" (INADDR_ANY en IPv4, IN6ADDR_ANY_INIT en IPv6) si ai_flags contient le bit AI_PASSIVE, et à l'adresse de "loopback" (INADDR_LOOPBACK ou IN6ADDR_LOOPBACK_INIT) sinon. Le cas AI_PASSIVE sert donc à obtenir des réponses utilisables par un programme serveur dans un bind pour recevoir des requêtes. Enfin si le bit AI_CANONNAME est positionné, le champ ai_canonname de la réponse contient le nom canonique de nodename.

La primitive getnameinfo remplace les primitives gethostbyaddr et getservbyport. Elle effectue la traduction d'une adresse vers un nom :

#include <sys/socket.h>
#include <netdb.h>
 
int getnameinfo(const struct sockaddr *sa, socklen_t salen,
char *host, size_t hostlen,
char *serv, size_t servlen, int flags);

En entrée l'argument sa pointe vers une structure d'adresse générique (de type sockaddr_in ou sockaddr_in6) et salen contient sa longueur. Le champ host (resp. serv) doit pointer sur une zone de longueur hostlen (resp. servlen) caractères. getnameinfo retourne la valeur 0 si tout est correct et un code d'erreur non nul si une erreur est détectée. S'il n'y a pas d'erreur, le champ host (resp. serv) reçoit en sortie le nom de la machine (resp. du service) correspondant. Les arguments host et serv peuvent être NULL si la réponse est inutile. Deux constantes sont définies pour permettre de réserver des zones de réponses de longueur raisonnable :

# define NI_MAXHOST 1025
# define NI_MAXSERV 32

Le champ flags permet de modifier la réponse : si flags contient le bit NI_NUMERICHOST (resp. NI_NUMERICSERV) la réponse sera l'adresse et non le nom de la machine (resp. le numéro et non le nom du service) ; si on ne sait pas trouver dans le serveur de nom le nom de la machine, getnameinfo rendra une erreur si le bit NI_NAMEREQD est positionné et l'adresse numérique sinon ; le bit NI_DGRAM indique si le service est sur UDP et non sur TCP.

Les fonctions de conversion numériques d'adresses

Elles sont l'analogue des fonctions inet_addr et inet_ntoa d'IPv4, la seule véritable différence étant qu'elles ont un argument précisant la famille d'adresse et peuvent donc aussi bien convertir les adresses IPv4 que les adresses IPv6. Comme la plupart des programmes manipulent des struct sockaddr*, il est souvent préferable d'utiliser les fonctions getaddrinfo et getnameinfo, au besoin avec le flag AI_NUMERICHOST.

#include <sys/socket.h>
#include <arpa/inet.h>
 
int
inet_pton(af, src, dst)
int af;              /* AF_INET ou AF_INET6 */
const char *src;     /* l'adresse (chaine de caract.) à traiter */
void *dst;           /* le tampon où est rangé le résultat */
 
char *
inet_ntop(af, src, dst, size)
int af;              /* AF_INET ou AF_INET6 */
const void *src;     /* l'adresse binaire à traiter */
char *dst;           /* le tampon où est rangé le résultat */
size_t size;         /* la taille de ce tampon */

La primitive inet_pton convertit une adresse textuelle en sa forme binaire. Elle retourne 1 lorsque la conversion a été réussie, 0 si la chaine de caractères qui lui a été fournie n'est pas une adresse valide et -1 en cas d'erreur, c'est-à-dire lorsque la famille d'adresses (premier argument) n'est pas supportée. Actuellement, les deux seules familles d'adresses supportées sont AF_INET et AF_INET6.

La primitive duale inet_ntop convertit une adresse sous forme binaire en sa forme textuelle. Le troisième argument est un tampon destiné à recevoir le résultat de la conversion. Il doit être d'une taille suffisante, à savoir 16 octets pour les adresses IPv4 et 46 octets pour les adresses IPv6. Ces deux tailles sont définies dans le fichier netinet/in.h :

#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46

Si la conversion est réussie, inet_ntop retourne un pointeur vers le tampon où est rangé le résultat de la conversion. Dans le cas contraire, inet_ntop retourne le pointeur nul, ce qui se produit soit lorsque la famille d'adresses n'est pas reconnue, soit lorsque la taille du tampon est insuffisante.

La commande haah (host-address-address-host)

L'exemple proposé n'est autre qu'une sorte de nslookup (très) simplifié. Si par exemple on lui donne en argument une adresse numérique (IPv4 ou IPv6), il imprime le nom complètement qualifié correspondant lorsque la requête DNS aboutit. L'extrait de session qui suit illustre l'utilisation de cette commande.

$ haah bernays
Canonical name:
bernays.ipv6.logique.jussieu.fr
Adresses:
2001:660:101:101:200:f8ff:fe31:17ec
3ffe:304:101:1:200:f8ff:fe31:17ec
$ haah 134.157.19.71
Canonical name:
bernays.logique.jussieu.fr
Adresses:
134.157.19.71
$

Le programme réalisant la commande haah ne présente aucune difficulté. C'est une simple application des primitives précédemment décrites.

 1| #include <stdio.h> </tt>
 2|
 3| #include <string.h>
 4| #include <errno.h>
 5| #include <sys/types.h>
 6| #include <sys/socket.h>
 7| #include <netinet/in.h>
 8| #include <netdb.h>
 9| #include <arpa/inet.h>
10|
11| 
12| int main(int argc, char **argv)
13| {
14|    int ret;
15|    struct addrinfo *res, *ptr;
16|    struct addrinfo hints = {
17|       AI_CANONNAME,
18|       PF_UNSPEC,
19|       SOCK_STREAM,
20|       0,
21|       0,
22|       NULL,
23|       NULL,
24|       NULL
25|    };
26|  	
27|    if (argc != 2) {
28|       fprintf(stderr, "%s: usage: %s host | addr.\n", *argv, *argv);
29|       exit(1);
30|    }
31|    ret = getaddrinfo(argv[1], NULL, &hints, &res);
32|    if (ret) {
33|       fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret));
34|       exit(1);
35|    }
37|    for (ptr = res; ptr; ptr = ptr->ai_next) {
38|       if (ptr->ai_canonname)
39|          fprintf(stdout,"Canonical name:\n%s\nAdresses:\n", ptr->ai_canonname);
40|       switch (ptr->ai_family) {
41|          case AF_INET:
42|          {
43|             char dst[INET_ADDRSTRLEN];
44|             struct in_addr *src = &((struct sockaddr_in *) ptr->ai_addr)->sin_addr;
45|      
46|             if(!inet_ntop(AF_INET, (const void *) src, dst, sizeof(dst))) {
47|                fprintf(stderr, "inet_ntop: %s\n", strerror(errno));
48|                break;
49|             }
50|             fprintf(stdout, "%s\n", dst);
51|             break;
52|          } 
53|          case AF_INET6:
54|          {
55|             char dst[INET6_ADDRSTRLEN];
56|             struct in6_addr *src=&((struct sockaddr_in6 *)ptr->ai_addr)->sin6_addr;
57|     
58|             if (!inet_ntop(AF_INET6, (const void *) src, dst, sizeof(dst))) {
59|                fprintf(stderr, "inet_ntop: %s\n", strerror(errno));
60|                break;
61|             }
62|             fprintf(stdout, "%s\n", dst);
63|             break;
64|          }
65|          default:
66|             fprintf(stderr, "getaddrinfo: %s\n", strerror(EAFNOSUPPORT));
67|       }
68|    } 
69|    freeaddrinfo(res);
70|    exit(0);
71| }

Exemple de client/serveur TCP

Vue d'ensemble

Le client/serveur choisi est particulièrement simple quant à sa fonction de service, de façon à privilégier l'aspect réseau dans la présentation. Il calcule le nombre d'utilisateurs connectés sur une machine donnée. Plus précisément :

$ nbus bernays
3 user(s) logged on bernays
$

Il se compose de cinq fichiers sources :

  • nbus.h le fichier d'en-tête commun aux fichiers sources du client et du serveur
  • nbus.c le fichier source principal du client
  • open_conn.c le fichier source qui gère les connexions du côté client
  • nbusd.c le fichier source du serveur propre au service
  • serv_daemon.c le fichier source qui gère les connexions du côté serveur

Le client, prend en argument un nom de machine (ou une adresse numérique), le convertit en une adresse IPv4 ou IPv6 et ainsi envoie ses requêtes à un serveur.

Le fichier source du serveur, selon les options de compilation, génère un code IPv6 ou un code IPv4. Dans le premier cas, le serveur est à même de satisfaire les requêtes d'un client IPv6 mais aussi d'un client IPv4. Dans le second cas, seuls les clients IPv4 pourront être pris en compte.

Il faut noter qu'il existe deux modes de fonctionnement pour une machine à double pile IPv4 et IPv6, la figure Le client/serveur nbus résume la situation. Soit les espaces d'adresses sont disjoints, et dans ce cas il faut deux serveurs, un qui écoute les requêtes IPv4 (nbus4d) et un qui écoute les requêtes IPv6 (nbus6d) (ou un seul serveur, nbusd, avec deux sockets IPv4 et IPv6 séparées). Soit l'espace d'adresse IPv4 est inclus dans l'espace IPv6 et dans ce cas il suffit d'un serveur IPv6 (nbus6d) qui recevra les requêtes venant en IPv4 comme une requête IPv6 venant d'une adresse "IPv4 mappée".

CS192.gif

Suivant les systèmes le mode de fonctionnement est prédéfini, configurable globalement dans le noyeau, ou configurable séparément pour chaque socket IPv6. Ainsi en FreeBSD le mode par défaut est "espace partagé" (choix modifiable par "sysctl -w net.inet6.ip6.v6only=1") et modifiable pour chaque socket, en précisant tcp4, tcp6 ou tcp46 dans le fichier de configuration d'inetd, ou en utilisant "setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY,&val)" dans le code du serveur.

Deux mêmes programmes serveur ne peuvent s'exécuter en même temps sur une machine : chaque serveur réserve le port nbus (par un bind) et par suite si un serveur est lancé, tout autre serveur échouera avec une erreur "port en service" (EADDRINUSE). Cela peut être aussi vrai entre les deux types de serveurs, nbus4d et nbus6d sur une machine "double pile avec espace partagé", car les espaces de service TCP et UDP sont communs à IPv4 et IPv6 et un port en service l'est aussi bien en IPv4 qu'en IPv6 ; simplement si le port correspond à une socket PF_INET, une requête de connexion IPv6 sera rejetée avec une erreur "port inaccessible". Dans la réalité le comportement est plus complexe. Même en mode "double pile avec espace partagé", on peut avoir deux sockets, l'une PF_INET et l'autre PF_INET6, comme le montre le programme nbusd.

Le code des différents serveurs est très semblable, le choix est fait en donnant une option à la compilation : nbusd par défaut, nbus4d si on ajoute l'option -DIPV4,et nbus6d si on ajoute l'option -DIPV6.

Le fichier d'en-tête nbus.h ne contient que le nom du service correspondant.

#define SERVICE "nbus"

Ainsi doit-on trouver, par exemple, dans le fichier /etc/services des machines concernées, la ligne suivante :

nbus 20000/tcp

Le programme client nbus établit tout d'abord une connexion TCP avec la machine cible (donnée en argument à nbus) via la fonction open_conn (cette fonction sera décrite ci-après). Il lit ensuite un entier court, résultat du calcul du nombre d'utilisateurs connectés sur la machine cible, puis l'imprime.

On notera la présence de la macro ntohs rendue nécessaire du fait des représentations différentes des entiers selon les machines.

 1| #include <stdio.h>
 2| #include <unistd.h>
 3|  
 4| #include "nbus.h"
 5|  
 6| extern open_conn(char *, char *);
 7|  
 8| int main(int argc, char **argv)
 9| {
10|    int sock;
11|    short nu; 
12| 
13|    if (argc != 2) {
14|       fprintf(stderr, "Usage: %s host\n", argv[0]);
15|       exit(1);
16|    }
17|    if ((sock = open_conn(argv[1], SERVICE)) < 0)
18|       exit(1);
19|    read(sock, (char *) &nu, sizeof(nu));
20|    nu = ntohs(nu);
21|    if (nu == -1) {
22|       fprintf(stderr, "Can't read \"utmp\" on %s\n", argv[1]);
23|       exit(1);
24|    }
25|    if (nu) {
26|       fprintf(stdout, "%d user(s) logged on %s\n", nu, argv[1]);
27|       exit(0);
28|    }
29|    fprintf(stdout, "Nobody on %s\n", argv[1]);
30|    exit(0);
31| }

Le serveur nbusd, quant à lui, lance en tâche de fond un processus "démon" qui exécutera la fonction de service nbus (lignes 28 à 60) à chaque requête d'un client. Ce processus démon est réalisé par la fonction serv_daemon.


 1| #include <stdio.h>
 2| #include <unistd.h>
 3| #include <fcntl.h>
 4| #include <utmp.h>
 5| #include <sys/socket.h> 
 6| 
 7| #include "nbus.h" 
 8|  
 9| 
10| #if defined(IPV6)
11| #define FAMILY PF_INET6
12| #elif defined(IPV4)
13| #define FAMILY PF_INET
14| #else
15| #define FAMILY PF_UNSPEC
16| #endif
17| 
18| extern serv_daemon(int, char *, void (*)(int), char *);
19| 
20| void nbus(int);
21| 
22| int main(void)
23| {
24|    serv_daemon(FAMILY, SERVICE, nbus, NULL);
25|   exit(0);
26| }
27| 
28| void nbus(int sock)
29| {
30|    short nu = -1;
31| #ifdef USER_PROCESS /* Solaris/Linux, use getutent */
32|    struct utmp *up;
33| 
34|    up = getutent();
35|    if (up != NULL) {
36|       for (nu = 0; up != NULL; up = getutent())
37|       if (up->ut_type == USER_PROCESS)
38|       nu++;
39|    }
40|    endutent();
41| #else /* *BSD read directly utmp file */
42| #ifndef UTMP
43| #define UTMP "/var/run/utmp" /* for FreeBSD/NetBSD */
44| #endif
45| 
46|    struct utmp ut;
47|    int fd;
48| 
49|   if ((fd = open(UTMP, O_RDONLY)) >= 0) {
50|       nu = 0;
51|       while (read(fd, (char *) &ut, sizeof(ut)) == sizeof(ut))
52|       if (ut.ut_name[0])
53|       nu++;
54|    }
55|    close(fd);
56| #endif
57|    nu = htons(nu);
58|    write(sock, (char *) &nu, sizeof(nu));
59|    return;
60| }

L'établissement d'une connexion TCP, côté client

Comme on l'a vu plus haut dans le code du client nbus.c, l'établissement de la connexion TCP se fait au moyen de la fonction open_conn. Cette fonction prend en premier argument le nom de la machine avec laquelle on va établir la connexion TCP et en deuxième argument le nom du service tel qu'il apparaît dans le fichier /etc/services. La valeur retournée par open_conn est soit -1 en cas d'erreur, soit le descripteur associé à la socket réalisant la connexion. La figure Algorithme du client visualise l'algorithme employé.

CS193.gif

La première étape sera la construction de l'adresse de la socket distante, ceci via la primitive getaddrinfo. On remarquera que le champ ai_family de la structure hints a été initialisé à la valeur PF_UNSPEC, ce qui signifie que, suivant que l'on donne en argument à la commande nbus un nom de machine (ou une adresse numérique) IPv4 ou IPv6, on travaillera avec une socket soit dans la famille des protocoles PF_INET, soit dans la famille des protocoles PF_INET6. Si on avait fait le choix de forcer la famille des protocoles à la valeur PF_INET6, il aurait fallu initialiser les champs ai_flags et ai_family respectivement aux valeurs AI_V4MAPPED et PF_INET6 (les adresses IPv4 seraient dans ce cas "mappées"). Si, comme dans l'exemple proposé, on ne fait pas ce choix, on notera qu'il n'y a aucune différence entre le code IPv4 et le code IPv6.

Ensuite, après avoir créé une socket, on la connecte au serveur de la machine cible (primitive connect). Là encore, le code est le même pour IPv4 et IPv6. Remarquons que getaddrinfo peut avoir rendu plusieurs valeurs. Un programme plus évolué devrait essayer de se connecter à chaque adresse rendue jusqu'à obtenir une réponse.


 1| #include <stdio.h>
 2| #include <unistd.h>
 3| #include <sys/socket.h>
 4| #include <netdb.h>
 5| 	
 6| int open_conn(char *host, char *serv)
 7| {
 8|    int sock, ecode;
 9|    struct addrinfo *res;
10|    struct addrinfo hints = {
11|       0,
12|       PF_UNSPEC,
13|       SOCK_STREAM,
14|       0,
15|       0,
16|       NULL,
17|       NULL,
18|       NULL
19|    };
20| 
21|    ecode = getaddrinfo(host, serv, &hints, &res);
22| 
23|    if (ecode) {
24|       fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ecode));
25|       exit(1);
26|    }
27| 
28|    if ((sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol)) < 0) {
29|       freeaddrinfo(res);
30|       perror("socket");
31|       return -1;
32|    }
33|  
34|   if (connect(sock, res->ai_addr, res->ai_addrlen) < 0) {
35|       close(sock);
36|       freeaddrinfo(res);
37|       perror("connect");
38|       return -1;
39|    }
40|    freeaddrinfo(res);
41|    return sock;
42| }

Le serveur

Le serveur proprement dit est réalisé par une unique fonction. C'est la fonction serv_daemon qui a quatre arguments. Le premier est la famille d'adresse gérée, PF_INET ou PF_INET6, ou PF_UNSPEC pour écouter dans les deux familles. Le deuxième est, comme dans le cas de open_conn, le nom du service tel qu'il apparaît dans le fichier /etc/services. Le troisième argument est un pointeur vers la fonction de service dont le type (int(*)(void)) sera commenté ultérieurement. Enfin le dernier argument est le nom passé au démon syslogd lors de l'impression des messages d'erreur (ligne See ). Si le dernier argument est le pointeur nul, ce nom est par défaut le nom du service. Un aperçu du déroulement de la fonction serv_daemon est donné par la figure Algorithme du serveur.

CS194.gif


  1| #include <stdio.h>
  2| #include <stdlib.h>
  3| #include <unistd.h>
  4| #include <errno.h>
  5| #include <sys/socket.h>
  6| #include <netdb.h>
  7| #include <signal.h>
  8| #include <syslog.h>
  9| #include <sys/select.h>
 10| #include <sys/wait.h>
 11|  
 12| static void reap_child(int); 
 13| 	
 14| void serv_daemon(int family, char *serv, void (*serv_funct)(int), char *serv_logname)
 15| {
 16|    int sock[2], ecode, n = 0;
 17|    struct addrinfo *res, *rres, hints;
 18|   
 19|    memset(&hints, 0, sizeof hints) ;
 20|    hints.ai_flags = AI_PASSIVE;
 21|    hints.ai_socktype = SOCK_STREAM;
 22|    hints.ai_family = family;
 23|    ecode = getaddrinfo(NULL, serv, &hints, &rres);
 24|    if (ecode) {
 25|       fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ecode));
 26|       exit(1);
 27|    }
 28|    for (res = rres; res; res = res->ai_next) {
 29|       if (n == 2) { /* au plus 2 : anyaddr PF_INET et anyaddr PF_INET6 */
 30|         fprintf(stderr, "erreur interne: trop d'adresses\n");
 31|         exit(1);
 32|       }
 33|       sock[n] = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
 34|       if (sock[n] < 0) {
 35|          perror("socket");
 36|          exit(1);
 37|       }
 38|       if (bind(sock[n], res->ai_addr, res->ai_addrlen) < 0) {
 39|          perror("bind");
 40|          exit(1);
 41|       }
 42|       listen(sock[n], SOMAXCONN);
 43|       n++;
 44|       #ifdef __linux__
 45|                       /* En Linux, utiliser seulement la premiere
 46|                        * reponse, sinon on a un conflit sur bind */
 47|       break;
 48|       #endif
 49|    }
 50|    freeaddrinfo(rres);


La première étape consiste en la préparation de l'adresse de la socket d'écoute du serveur en faisant appel, comme d'habitude, à la primitive getaddrinfo. Comme il s'agit d'une socket d'écoute, le champ ai_flags de la structure hints a été initialisé à la valeur AI_PASSIVE tandis que le champ ai_family de cette même structure a lui été initialisé à la valeur passé en argument à l'appel.

Pour chaque réponse de getaddrinfo (une seule si la famille est définie, deux si la famille est PF_UNSPEC), on crée une socket d'écoute, puis effectue son attachement en faisant appel à la primitive bind. La socket d'écoute créée et attachée à une adresse, le serveur doit signifier au système qu'il est prêt à accepter les demandes de connexions. C'est l'objet de la primitive listen dont le deuxième argument SOMAXCONN (macro-définition que l'on trouve dans le fichier sys/socket.h) est le nombre maximal de connexions pendantes.

Dans les lignes qui suivent (51 à 58), le processus est détaché du terminal de contrôle et est lancé en tâche de fond (processus "démon"), sauf en Solaris, le code étant trop différent, si le fichier source n'est pas compilé avec l'option -DDEBUG (dans le cas contraire, cela peut être bien utile lors de la phase de mise au point).


 51|    #ifndef DEBUG
 52|    #ifndef sun /* no daemon function in Solaris */
 53|    if (daemon(0, 0) < 0) {
 54|       perror("daemon");
 55|       exit(1);
 56|    }
 57|    #endif
 58|    #endif 

Dans la boucle sans fin (lignes See à See ), on attend les connexions. Comme il peut y avoir plusieurs sockets actives, on utilise select (lignes See à See ) pour attendre sur toutes les sockets ouverts. Au retour de select, on teste les sockets qui ont des connexions pendantes (ligne See ), chaque connexion pendante dans la file d'attente associée à la socket d'écoute est extraite par le serveur et le circuit virtuel avec la socket du client est établi via la création d'une nouvelle socket. Ce travail est effectué par la primitive accept dont la valeur de retour est le descripteur d'E/S de la socket nouvellement créée. Ensuite le serveur crée un processus fils (ligne See ) qui exécutera la fonction de service à laquelle on passe en argument la valeur retournée par la primitive accept.

Il faut également veiller à ce que chaque processus fils, lors de sa terminaison (qui se produit à la fin de l'exécution de la fonction de service), ne devienne un processus "zombie", ce qui à terme peut provoquer une saturation de la table des processus. Pour cela il faut, sous UNIX BSD, capter le signal SIGCHLD (ligne See , fonction reap_child). Notons qu'en SYSTEM V, on pourrait simplement ignorer le signal SIGCLD.


 59|       signal(SIGCHLD, reap_child);
 60|       if (!serv_logname)
 61|          serv_logname = serv;
 62|       openlog(serv_logname, LOG_PID | LOG_CONS, LOG_USER);
 63|       for (;;) {
 64|          int a, f, len, fd, m = -1;
 65|          struct sockaddr_storage from;
 66|          fd_set fdset;
 67|   
 68|          FD_ZERO(&fdset);
 69|          for (fd = 0; fd < n; fd++) {
 70|             if (m < sock[fd])
 71|                m = sock[fd];
 72|             FD_SET(sock[fd], &fdset);
 73|          }
 74|          ecode = select(m+1, &fdset, NULL, NULL, NULL);
 75|          if (ecode < 0)
 76|             syslog(LOG_ERR, "%s: select: %m", serv);
 77|          if (ecode <= 0)
 78|             break;
 79|          for (fd = 0; fd < n; fd++) {
 80|            if (FD_ISSET(sock[fd], &fdset)) {
 81|               len = sizeof from;
 82|               a = accept(sock[fd], (struct sockaddr *)&from, &len);
 83|               if (a < 0) {
 84|                  if (errno != EINTR)
 85|                   syslog(LOG_ERR, "%s: accept: %m", serv);
 86|                   continue;
 87|               }
 88|               f = fork(); 
 89|               if (f == 0) {
 80|                  /* Par correction, il faudrait fermer dans le fils
 90|                   * tous les descripteurs hérités (i.e. tous sauf a) */
 91|                  serv_funct(a);
 92|                  exit(0);
 93|               }
 94|               close(a);
 95|               if (f == -1) {
 96|                   syslog(LOG_ERR, "%s: fork: %m", serv);
 97|                   continue;
 98|               }
 99|           }
100|        }
101|    }
102| }
103|   
104| static void reap_child(int sig)
105| {
106|    int status;
107|  
108|    while (wait3(&status, WNOHANG, NULL) > 0);
109| }

Une dernière remarque toujours à propos de la similitude des codes IPv6 et IPv4 : la seule différence dans le code de serv_daemon entre IPv4 et IPv6 est la valeur du premier argument, qui définit la famille d'adresses écoutée.

Mini-ping

Description

La commande proposée est une version très simplifiée de ping6. Néanmoins, cela permettra de comprendre l'essentiel du fonctionnement de cette commande. Son principe est le suivant, on émet un paquet ICMPv6 du type ECHO_REQUEST et on active une temporisation. Si, le délai étant expiré, on n'a pas reçu de paquet ICMPv6 de type ECHO_REPLY en provenance de la machine cible, on imprime un message d'erreur. Dans le cas contraire, on imprime le nom de la machine émettrice de l'ECHO_REPLY. Par exemple, si le nom donné à cette commande est one_ping6 :

$ one_ping6 peirce
Sending ECHO REQUEST to: peirce.ipv6.logique.jussieu.fr
Waiting for answer (timeout = 5s)...
Got answer from 2001:660:101:201:200:f8ff:fe31:1942 (seq = 0)
$

Remarque : ICMP étant un protocole non fiable, il peut arriver qu'un premier paquet soit perdu, par exemple à cause du temps passé à exécuter le protocole de "recherche de voisins". Il suffit en général de relancer la commande pour que la réponse apparaisse la seconde fois.

one_ping6 accepte les options suivantes :

  • -d données. Ces données seront incluses dans le paquet ECHO_REQUEST.
  • -s numéro de séquence. La valeur défaut est zéro.
  • -t durée de la temporisation. La valeur par défaut est fixée lors de la compilation via la macro-définition TIMEOUT.

Par exemple,

$ one_ping6 -d 'Un petit essai' -s 12 -t 3 peirce
Sending ECHO REQUEST to: peirce.ipv6.logique.jussieu.fr
Waiting for answer (timeout = 3s)...
Got answer from 2001:660:101:201:200:f8ff:fe31:1942 (seq = 12)
with data [
Un petit essai
] (end of data)
$

Les sources de ce programme se composent de trois fichiers : le programme principal, le source de la fonction assurant l'émission du paquet ECHO_REQUEST et le source de la fonction ayant en charge la gestion de la temporisation et la réception du paquet ECHO_REPLY.

Envoi du paquet ECHO_REQUEST

Rappelons tout d'abord que le nouveau protocole ICMPv6 est une refonte presque complète d'ICMP (sur IPv4). Néanmoins, le format des paquets ECHO_REQUEST et ECHO_REPLY est inchangé excepté la valeur du champ type (cf. figure format d'un message ICMPv6 demande et réponse d'écho).

CS43.gif

La préparation d'un paquet ECHO_REQUEST est similaire en ICMP(v4) ou ICMPv6. La seule différence est que le calcul du checksum n'est maintenant plus à la charge du programmeur mais effectué par le noyau. Plus précisément, ainsi qu'il est spécifié dans l'API "avancée", pour toutes les sockets de type SOCK_RAW et de protocole IPPROTO_ICMPV6, c'est le noyau qui doit calculer le checksum des paquets ICMPv6 sortants (dans le cas des Linux anciens, il faut activer le calcul du checksum, comme on le voit en lignes 81 à 95 du fichier ping.c ).

Le paquet ICMPv6 de type ECHO_REQUEST, étant ainsi constitué, on l'expédie, via la primitive sendto à la machine cible.


 1| #include <stdio.h>
 2| #include <string.h>
 3| #include <sys/types.h>
 4| #include <sys/socket.h>
 5| #include <netinet/in.h>
 6| #include <netinet/ip6.h>
 7| #include <netinet/icmp6.h>
 8| #include <arpa/inet.h>
 9| #include <netdb.h>
10|  
11| #ifndef MAX_DATALEN
12| #define MAX_DATALEN (1280 - sizeof(struct ip6_hdr) - sizeof(struct icmp6_hdr))
13| #endif
14|  
15| static u_char buf[sizeof(struct icmp6_hdr) + MAX_DATALEN]; 
16| 
17| int send_echo_request6(int sock, struct sockaddr_in6 *dst, uint16_t id,
18|                        uint16_t seq, char *opt_data, int opt_data_size)
19| {
20| int noc, icmp_pkt_size = sizeof(struct icmp6_hdr);
21| struct icmp6_hdr *icmp;   
22| 
23|    if (opt_data && opt_data_size > MAX_DATALEN) {
24|       fprintf(stderr, "send_echo_request6: too much data (%d > %d)\n",
25|       opt_data_size, MAX_DATALEN);
26|       return -1;
27|    }
28| 
29|    memset((void *) buf, 0, sizeof(buf));
30|    icmp = (struct icmp6_hdr *) buf;
31|    icmp->icmp6_type = ICMP6_ECHO_REQUEST;
32|    icmp->icmp6_id = id;
33|    icmp->icmp6_seq = seq;
34|    if (opt_data) {
35|       memcpy(buf + sizeof(struct icmp6_hdr), opt_data, opt_data_size);
36|       icmp_pkt_size += opt_data_size;
37|    } 
39|    noc = sendto(sock, (char *) icmp, icmp_pkt_size, 0,
39|                 (struct sockaddr *) dst, sizeof(struct sockaddr_in6));
40|    if (noc < 0) {
41|       perror("send_echo_request6: sendto");
42|       return -1;
43|    }
44|    if (noc != icmp_pkt_size) {
45|       fprintf(stderr, "send_echo_request6: wrote %d bytes, ret=%d\n",
46|               icmp_pkt_size, noc);
47|       return -1;
48|    }
49|    return 0;
50| }

Une dernière remarque avant de clore cette section. On a vu que l'on pouvait inclure des données dans le paquet ICMPv6 émis. La taille maximale de celles-ci a été choisie (ligne 12) pour que les paquets ne soient jamais fragmentés (le protocole IPv6 exigeant une taille de paquet minimale de 1280 octets, en-têtes comprises). Une taille plus grande serait possible, les paquets ICMP ECHO pouvant parfaitement être fragmentés.

La réception du paquet ECHO_REPLY

C'est la fonction wait_for_echo_reply6 qui gère la réception du paquet ECHO_REPLY. Cette fonction tout d'abord (lignes 32 à 35) utilise le mécanisme de filtrage des paquets ICMPv6, mécanisme défini dans l'API "étendue", afin que seuls les paquets ICMPv6 de type ECHO_REPLY soient reçus sur la socket d'écoute.

On trouve ensuite une boucle sans fin dont on sort soit sur réception du signal SIGALRM (armé juste avant l'entrée de la boucle à la ligne 36), c'est-à-dire lorsque le délai de temporisation (argument timeout) est expiré, soit lorsque la fonction recv_icmp_pkt, qui analyse tous les paquets ICMPv6 de type ECHO_REPLY reçus sur la socket d'écoute (argument sock) par l'émetteur, retourne 0, c'est-à-dire lorsque le paquet ECHO_REPLY en provenance de la machine cible a été détecté.


  1| #include <stdio.h>
  2| #include <unistd.h>
  3| #include <string.h>
  4| #include <sys/types.h>
  5| #include <sys/socket.h>
  6| #include <netinet/in.h>
  7| #include <netinet/ip6.h>
  8| #include <netinet/icmp6.h>
  9| #include <arpa/inet.h>
 10| #include <errno.h>
 11| #include <signal.h>
 12| #include <setjmp.h>
 13|  
 14| #ifndef MAX_DATALEN
 15| #define MAX_DATALEN (1280 - sizeof(struct ip6_hdr) - sizeof(struct icmp6_hdr))
 16| #endif
 17|  
 18| static void on_timeout(int);
 19| static int recv_icmp_pkt(int, struct sockaddr_in6 *, uint16_t, uint16_t);
 20|  
 21| static u_char buf[sizeof(struct icmp6_hdr) + MAX_DATALEN];
 22| static jmp_buf j_buf;
 23|  
 24| void wait_for_echo_reply6(int sock, struct sockaddr_in6 *from, uint16_t id,
 25|                           uint16_t seq, int timeout)
 26| {
 27| struct icmp6_filter filter;
 28| char from_ascii[INET6_ADDRSTRLEN];
 29|  
 30|    inet_ntop(AF_INET6, &from->sin6_addr, from_ascii, INET6_ADDRSTRLEN);
 31|  
 32|    ICMP6_FILTER_SETBLOCKALL(&filter);
 33|    ICMP6_FILTER_SETPASS(ICMP6_ECHO_REPLY, &filter);
 34|    setsockopt(sock, IPPROTO_ICMPV6, ICMP6_FILTER, (const void *) &filter,
 35|               sizeof(filter));
 36|    signal(SIGALRM, on_timeout);
 37|    alarm(timeout);
 38|    for (;;) {
 39|       int noc, from_len = sizeof(struct sockaddr_in6);
 40|  
 41|       if (setjmp(j_buf) == SIGALRM) {
 42|          fprintf(stderr, "No answer from %s\n", from_ascii);
 43|          break;
 44|       }
 45|       noc = recvfrom(sock, buf, sizeof(buf), 0,
 46|                      (struct sockaddr *) from, &from_len);
 47|       if (noc < 0) {
 48|          if (errno == EINTR)
 49|             continue;
 50|          perror("wait_for_echo_reply6: recvfrom");
 51|          continue;
 52|       }
 53|       if (recv_icmp_pkt(noc, from, id, seq) == 0)
 54|          break;
 55|    }
 56|    alarm(0);
 57|    signal(SIGALRM, SIG_DFL);
 58|    return;
 59| }
 60|  
 61| static void on_timeout(int sig)
 62| {
 63|    longjmp(j_buf, sig);
 64| } 

Contrairement à ce qui se passait en IPv4, l'entête IPv6 n'est pas incluse lors de la réception d'un paquet ICMPv6 (sauf si l'option IP_HDRINCL est positionnée). Ainsi dans la fonction recv_icmp_pkt, on commence directement par tester le champ identificateur et le numéro de séquence (lignes 84 et 85). Si ce test a été passé avec succès, c'est-à-dire que l'on a bien reçu le paquet attendu, la fonction recv_icmp_pkt retourne 0 après avoir, s'il y en a, imprimé les données incluses dans le paquet. Dans le cas contraire, la valeur retournée est 1.


 65| static int recv_icmp_pkt(int noc, struct sockaddr_in6 *from, uint16_t id,
 66|                          uint16_t seq)
 67| {
 68| int opt_data_size;
 69| char from_ascii[INET6_ADDRSTRLEN];
 70| struct icmp6_hdr *icmp;
 71|  
 72|    if (inet_ntop(AF_INET6, &from->sin6_addr, from_ascii,
 73|                  INET6_ADDRSTRLEN) == NULL) {
 74|       perror("inet_ntop");
 75|       return -1;
 76|    }
 77|    if (noc < sizeof(struct icmp6_hdr)) {
 78|       fprintf(stderr, "recv_icmp_pkt: packet too short from %s\n",
 79|               from_ascii);
 80|       return -1;
 81|    }
 82|    opt_data_size = noc - sizeof(struct icmp6_hdr);
 83|    icmp = (struct icmp6_hdr *) buf;
 84|    if (icmp->icmp6_id != id || icmp->icmp6_seq != seq)
 85|       return 1;
 86|    fprintf(stdout, "Got answer from %s (seq = %d)\n", from_ascii, seq);
 87|    if (opt_data_size > 0) {
 88|       fprintf(stdout, "with data [\n");
 89|       fflush(stdout);
 90|       if (opt_data_size > MAX_DATALEN) {
 91|          fprintf(stderr,
 92|                  "recv_icmp_pkt: received too much data from %s\n",
 93|                  from_ascii);
 94|       }
 95|       else
 96|          write(1, (char *) icmp + sizeof(struct icmp6_hdr), opt_data_size);
 97|       fprintf(stdout, "\n] (end of data)\n");
 98|    }
 99|    return 0;
100| }

Programme principal

Le programme principal ne présente pas de difficulté particulière puisqu'il est une application directe des fonctions décrites dans les deux sections précédentes.

La première partie est triviale : elle concerne le traitement des (éventuelles) options.


  1| #include <stdio.h>
  2| #include <stdlib.h>
  3| #include <unistd.h>
  4| #include <string.h>
  5| #include <sys/socket.h>
  6| #include <netinet/in.h>
  7| #include <netinet/icmp6.h>
  8| #include <arpa/inet.h>
  9| #include <netdb.h>
 10| #ifdef __linux__
 11| #include <linux/version.h>
 12| #if LINUX_VERSION_CODE < KERNEL_VERSION(2,4,19)
 13| #define LINUX_CKSUM_CALCUL_EXPLICITE
 14| #endif
 15| #endif
 16|  
 17| #ifndef TIMEOUT
 18| #define TIMEOUT 5
 19| #endif
 20|  
 21| extern int send_echo_request6(int, struct sockaddr_in6 *, uint16_t,
 22|                               uint16_t, char *, int);
 23| extern void wait_for_echo_reply6(int, struct sockaddr_in6 *, uint16_t,
 24|                                  uint16_t, int);
 25|  
 26| static void usage(char *);
 27|  
 28| int main(int argc, char **argv)
 29| {
 30| int sock, timeout = TIMEOUT, a, ecode;
 31| char *opt_data = NULL, *dst_ascii;
 32| int opt_data_size = 0;
 33| uint16_t id, seq = 0;
 34| struct sockaddr_in6 *dst;
 35| struct addrinfo *res;
 36| struct addrinfo hints = {
 37|                          AI_CANONNAME,
 38|                          PF_INET6,
 39|                          SOCK_RAW,
 40|                          IPPROTO_ICMPV6,
 41|                          0,
 42|                          NULL,
 43|                          NULL,
 44|                          NULL
 45|                         };
 46|  
 47|    while((a = getopt(argc, argv, "d:s:t:")) != EOF)
 48|       switch(a) {
 49|       case 'd':
 50|          opt_data = optarg;
 51|          opt_data_size = strlen(optarg) + 1;
 52|          break;
 53|       case 's':
 54|          seq = (uint16_t) atoi(optarg);
 55|          break;
 56|       case 't':
 57|          timeout = atoi(optarg);
 58|          break;
 59|       default:
 60|          usage(*argv);
 61|       }
 62|       argc -= optind;
 63|       if (argc != 1)
 64|          usage(*argv);
 65|       argv += optind; 

Ensuite c'est la préparation de l'adresse de la socket distante, opération qui est devenue maintenant familière. Noter que l'on a affecté au champ ai_family de la structure hints la valeur PF_INET6 lors de sa déclaration (ligne 38) : on doit s'assurer que la machine cible est une machine IPv6 (il n'existe pas de mode double pile avec utilisation d'adresse IPv4 mappé pour le protocole ICMP, car celui-ci a fortement changé entre IPv4 et IPv6). On s'est interdit des adresses destination de type multicast (lignes 73 à 76) car, comme l'on ne traite qu'un paquet en réception, cela n'aurait guère d'intérêt.

On crée la socket qui servira à l'émission du paquet ECHO_REQUEST et à la réception du paquet ECHO_REPLY en provenance de la machine cible.

À la ligne 96, la valeur du champ identificateur du paquet ICMPv6 est calculée en fonction du numéro de processus en prenant les 16 premiers bits. C'est une technique sûre (et simple) quant à la garantie de l'unicité de l'identificateur. Enfin le paquet ECHO_REQUEST est émis (send_echo_request6) puis on attend la réponse éventuelle (wait_for_echo_reply6).


 66|       ecode = getaddrinfo(*argv, NULL, &hints, &res);
 67|       if (ecode) {
 68|          fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ecode));
 69|          exit(1);
 70|       }
 71|       dst_ascii = res->ai_canonname ? res->ai_canonname : *argv;
 72|       dst = (struct sockaddr_in6 *) res->ai_addr;
 73|       if (IN6_IS_ADDR_MULTICAST(&dst->sin6_addr)) {
 74|          fprintf(stderr, "%s multicast address not supported\n", dst_ascii);
 75|          exit(1);
 76|       }
 77|       if ((sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol)) < 0) {
 78|          perror("socket (RAW)");
 79|          exit(1);
 80|       }
 81| #ifdef LINUX_CKSUM_CALCUL_EXPLICITE
 82|       {
 83|       /*
 84|        * Pour linux avant 2.4.19, il faut demander le calcul des checksums
 85|        * sur les sockets raw, meme pour des paquets icmpv6
 86|        */
 87| #define OFFSETOF(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
 88|        int off = OFFSETOF(struct icmp6_hdr, icmp6_cksum);
 89|  
 90|           if (setsockopt(sock, SOL_RAW, IPV6_CHECKSUM, &off, sizeof off) < 0) {
 91|              perror("setsockopt (IPV6_CHECKSUM)");
 92|              exit(1);
 93|           }
 94|      }
 95| #endif
 96|      id = (uint16_t) (getpid() & 0xffff);
 97|      fprintf(stdout, "Sending ECHO REQUEST to: %s\n", dst_ascii);
 98|      if (send_echo_request6(sock, dst, id, seq, opt_data,
 99|                             opt_data_size) < 0)
100|         exit(1);
101|      fprintf(stdout, "Waiting for answer (timeout = %ds)...\n", timeout);
102|      wait_for_echo_reply6(sock, dst, id, seq, timeout);
103|      close(sock);
104|      exit(0);
105| }
106|  
107| static void usage(char *s)
108| {
109|    fprintf(stderr, "Usage: %s [-d data] [-s seq] [-t timeout] host | addr\n", s);
110|    exit(1);
111| }

Utilisation du multicast

La programmation avec les groupes multicast n'est pas standardisée en IPv4. La nouvelle API "socket" propose un ensemble de structures et appels systèmes pour étendre l'interface de programmation sockets aux applications utilisant le multicast. Cet exemple va illustrer ce point.

Le but des deux programmes est d'échanger des données par multicast. Le programme in2multi6 diffuse les données lues en entrée ("standard input") vers un groupe multicast donné. Le programme multi2out6 écoute les paquets transmis dans ce groupe et les copie sur la sortie standard ("stdout").

multi2out6

Pour ce faire multi2out6 va faire des appels systèmes qui vont produire des paquets d'abonnement (et de désabonnement) à un groupe multicast. L'abonnement sera réalisé grâce à l'envoi de deux messages ICMPv6 successifs de type 131, c'est-à-dire des "rapports d'abonnement". Puis les messages émis dans le groupe sont reçus par le programme. Lorsque le programme s'arrête, le code de l'interface va automatiquement provoquer l'émission d'un message de réduction d'un groupe de multicast (132).

Le programme multi2out6 est appelé de la manière suivante :

multi2out6 [-i interface] <adresse de groupe multicast>

Voici le code complet du programme. Le port utilisé (ligne 15) est quelconque mais ne doit bien sûr pas correspondre à un service déjà existant.

 1| #include <sys/types.h>
 2| #include <sys/socket.h>
 3| #include <netinet/in.h>
 4| #include <arpa/inet.h>
 5| #include <stdio.h>
 6| #include <signal.h>
 7| #include <unistd.h>
 8|  
 9| #ifndef SO_REUSEPORT
10| #define SO_REUSEPORT SO_REUSEADDR
11| #endif
12|   
13| struct sockaddr_in6 sin6;
14|  
15| #define IPPORT 54321
16|  
17| void Perror(const char *c)
18| {
19|    perror(c);
20|    exit(1);
21| }
22|  
23| void Usage ()
24| {
25|    fprintf(stderr, "%s\n", "Usage: multi2out6 [-i interface] addr");
26|    exit(1);
27| }
28|  
29| void BrokenPipe(int Signal)
30| {
31|    signal(SIGPIPE, BrokenPipe);
32|    return;
33| }
34| 
 

La partie principale du programme traite les options éventuelles. En fait, il n'y en a qu'une, le choix de l'interface à abonner au groupe multicast. Une fois ces operations effectuées, il faut créer et attacher une socket à une adresse. Ces opérations sont réalisées par l'utilisation des fonctions classiques socket et bind.

On utilise une structure de données spéciale pour stocker l'adresse multicast du groupe (définie dans netinet/in.h) :

struct ipv6_mreq {
struct in6_addr ipv6mr_multiaddr; /* IPv6 mcast address of group */
unsigned int ipv6mr_interface; /* local IPv6 address of interface */
};



35| int main(int argc, char **argv)
36| {
37| struct ipv6_mreq mreq;
38| int cc, ccb, ch, s;
39| char buf[10240];
40| u_int one = 1;
41| u_int ifi = 0;
42|  
43|    signal(SIGPIPE, BrokenPipe);
44|    while ((ch = getopt(argc, argv, "i:")) != -1)
45|       switch(ch) {
46|       case 'i':
47|          if (sscanf(optarg, "%u\0", &ifi) != 1 &&
48|                       (ifi = if_nametoindex(optarg)) == 0)
49|             Usage();
50|          break;
51|       default:
52|          Usage();
53|       }
54|    argc -= optind;
55|    argv += optind;
56|    if (argc != 1)
57|       Usage();
58|  
59|    if ((s = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP)) < 0)
60|       Perror("socket");
61|    setsockopt(s, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one));
62|  
63| #ifdef SIN6_LEN
64|    sin6.sin6_len = sizeof(sin6);
65| #endif
66|    sin6.sin6_family = AF_INET6;
67|    sin6.sin6_port = htons(IPPORT);
68|    if (bind(s, (struct sockaddr *)&sin6, sizeof(sin6)) < 0)
69|       Perror("bind");

La fonction inet_pton va permettre la conversion du nom de groupe passé en option sous une forme textuelle (par exemple ff12::1234:5678) en forme numérique. Le résultat est directement stocké dans la variable mreq qui sera utilisée par la commande setsockopt. On passe en paramètre à cette fonction l'option IPV6_JOIN_GROUP avec la variable mreq. À partir de ce moment, il y a émission de deux messages d'abonnement. La boucle qui suit va permettre la lecture des informations envoyées sur le groupe auquel on vient de s'abonner et les afficher sur la sortie standard ainsi que leur longueur sur la sortie erreur standard.


70|    if (inet_pton(AF_INET6, *argv, &mreq.ipv6mr_multiaddr) != 1)
71|       Usage();
72|    mreq.ipv6mr_interface = ifi;
73|    if (setsockopt(s,IPPROTO_IPV6, IPV6_JOIN_GROUP, &mreq, sizeof(mreq)) < 0)
74|       Perror("setsockopt IPV6_JOIN_GROUP");
75|    for (;;) {
76|       cc = read(s, buf, 10240);
77|       if (cc < 0)
78|          Perror("read socket");
79|       if (cc == 0) {
80|          fprintf(stderr, "..\n");
81|          exit (0);
82|       }
83|       ccb = write(1, buf, cc);
84|       if (ccb != cc)
85|          Perror("write file");
86|       fprintf(stderr, "<-%d-\n", cc);
87|    }
88| }

Lorsque le programme s'arrête, un close(s) implicite a lieu, et le code de l'interface va envoyer un message de réduction de groupe si elle est la dernière à avoir envoyé un rapport d'abonnement au groupe.

in2multi6

Le programme est appelé de la manière suivante :

in2multi6 [-i interface][-h max-hop-count][-l loop] <adresse de groupe multicast>

Le code est relativement simple, principalement une analyse des arguments, le positionnement d'option et une boucle lecture--émission. En effet il n'est pas nécessaire de s'abonner pour faire de l'émission multicast.

Il y a quatre arguments, trois optionnels qui sont l'interface d'émission (nom ou index numérique), le "ttl" mis dans les paquets multicast (voir le manuel de la primitive readv), et un drapeau qui sert à dire si la machine émettrice reçoit ou non les paquet émis. Le dernier argument est l'adresse du groups sous forme numérique.

Voici le code complet du programme. Le port utilisé (ligne 10) est naturellement le même que celui de multi2out6.


 1| #include <sys/types.h>
 2| #include <sys/socket.h>
 3| #include <netinet/in.h>
 4| #include <arpa/inet.h>
 5| #include <stdio.h>
 6| #include <unistd.h>
 7|  
 8| struct sockaddr_in6 sin6;
 9|  
10| #define IPPORT 54321
11|  
12| void Perror(const char *c)
13| {
14|    perror(c);
15|    exit(1);
16| }
17|  
18| void Usage ()
19| {
20|    fprintf(stderr, "%s\n", "Usage: in2multi6 [-i interface][-h hop][-l loop] addr");
21|    exit(1);
22| }
23|  
24| int main(int argc, char **argv)
25| {
26| u_int hops = 1,       /* as defined in rfc2553 */
27|       loop = 1,       /* as defined in rfc2553 */
28|       ifi = 0;
29| int s, cc, ch;
30| char buf[1024];
31| struct in6_addr addr6;
32| extern char *optarg;
33| extern int optind;
34|  
35|    addr6 = in6addr_any;
36|    if ((s = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP)) < 0)
37|       Perror("socket");
38|    while ((ch = getopt(argc, argv, "h:t:l:i:")) != -1)
39|       switch(ch) {
40|       case 'h':
41|       case 't':
42|          hops = atoi(optarg);
43|          break;
44|       case 'l':
45|          loop = atoi(optarg);
46|          break;
47|       case 'i':
48|          if (sscanf(optarg, "%u\0", &ifi) != 1) {
49|             ifi = if_nametoindex(optarg);
50|             if (ifi == 0)
51|                Usage();
52|          }
53|          break;
54|       default:
55|          Usage();
56|    }
57|    argc -= optind;
58|    argv += optind;
59|    if (argc != 1 || inet_pton(AF_INET6, *argv, &addr6) <= 0)
60|       Usage();
61|    if (setsockopt(s, IPPROTO_IPV6, IPV6_MULTICAST_HOPS,
62|                   &hops, sizeof(hops)) < 0)
63|       Perror("setsockopt IPV6_MULTICAST_HOPS");
64|    if (setsockopt(s, IPPROTO_IPV6, IPV6_MULTICAST_LOOP,
65|                   &loop, sizeof(loop)) < 0)
66|       Perror("setsockopt IPV6_MULTICAST_LOOP");
67|    if (ifi && (setsockopt(s, IPPROTO_IPV6, IPV6_MULTICAST_IF,
68|                           &ifi, sizeof(u_int)) < 0))
69|       Perror("setsockopt IPV6_MULTICAST_IF");
70|  
71| #ifdef SIN6_LEN
72|    sin6.sin6_len = sizeof(sin6);
73| #endif
74|    sin6.sin6_family = AF_INET6;
75|    sin6.sin6_addr = addr6;
76|    sin6.sin6_port = htons(54321);
77|  
78|    for (;;) {
79|       cc = read(0, buf, 1024);
80|       if (cc < 0)
81|          Perror("read file");
82|       if (cc == 0) {
83|          fprintf(stderr, ".\n", cc);
84|          exit (0);
85|       }
86|       if (sendto(s, buf, cc, 0,
87|                  (struct sockaddr *)&sin6, sizeof(sin6)) < 0)
88|          Perror("sendto");
89|       fprintf(stderr, "-%d->\n", cc);
90|    }
91| }

Programmation avancée

L'API « avancée » (Advanced API), définie par le RFC 3542, a pour objet la standardisation de la manipulation en émission/réception des datagrammes IPv6. Elle permet notamment au programmeur d'écrire des applications utilisant les nouvelles fonctionnalités proposées par le protocole IPv6 et ce de façon portable.

Cette API avancée concerne essentiellement les sockets de type SOCK_DGRAM (UDP) ou de type SOCK_RAW (ICMPv6,...). En effet, comme il n'y a pas de correspondance biunivoque entre les opérations de réception (respectivement d'émission) et les segments TCP reçus (respectivement émis), la plupart des options proposées ne sont pas applicables ou voire dénuées de sens pour une socket de type SOCK_STREAM. L'API avancée est utile pour programmer des applications comme ping, traceroute, des implémentations de protocoles de routage et, de manière générale, toute application construite avec des sockets de type SOCK_RAW et devant accéder aux champs des en-têtes IPv6 ou ICMPv6.

La standardisation des appels systèmes et de fonctions a pour but de fournir un interface uniforme, évitant ainsi l'hétérogénéité qui existe en IPv4.

Les opérations disponibles sont les suivantes :

  • Calcul/vérification des checksums par le noyau (pour les sockets de type SOCK_RAW)
  • Filtrage des réceptions des paquets ICMPv6
  • Modification des caractéristiques du datagramme IPv6 (packet information)
  • Manipulation des en-têtes d'extension IPv6
  • proche-en-proche (hop-by-hop)
  • routage par la source (routing header)
  • destination (destination)
  • Gestion du MTU et du mécanisme de découverte du PMTU (Path MTU discovery)

En outre, des fonctions facilitant le traitement des en-têtes d'extension IPv6 ont été définies ainsi qu'une interface étendant les primitives rresvport, rcmd et rexec à IPv6. Cette API avancée ne prend pas en compte les en-têtes d'extension IPv6 liés à IPsec.

L'implémentation de ce nouveau standard est réalisé à l'aide des primitives sendmsg et recvmsg, les données en émission/réception étant traitées via les données auxiliaires (ancillary data) associées à la socket et gérées par ces primitives. Également, de nouvelles options ont été définies pour les sockets.

L'implémentation

Comme indiqué précédemment, l'implémentation de l'API avancée repose principalement sur les primitives sendmsg et recvmsg dont les prototypes sont les suivants :

int sendmsg(int s, const struct msghdr *msg, int flags);
int recvmsg(int s, struct msghdr *msg, unsigned int flags);

Le premier paramètre s désigne le descripteur d'E/S associée à la socket et le dernier paramètre flags est identique au 3ème paramètre des primitives sendto et recvfrom. Le second paramètre est une structure définie (dans <sys/socket.h>) comme suit :

struct msghdr {
   void *msg_name;           /* pointeur vers l'adresse de la socket */
   socklen_t msg_namelen;    /* longueur de l'adresse de la socket */
   struct iovec *msg_iov;    /* tampon mémoire vectoriel (scatter/gather array) */
   int msg_iovlen;           /* nombre d'éléments de msg_iov */
   void *msg_control;        /* données auxiliaires */
   socklen_t msg_controllen; /* longueur des données auxiliaires */
   int msg_flags;            /* drapeaux des messages reçus */
};

Les deux premiers champs spécifient pour sendmsg (respectivement recvmsg) l'adresse de destination (respectivement d'origine). Le premier champ peut être le pointeur NULL en mode connecté. Les deux champs suivants contiennent le tampon mémoire vectoriel en émission ou en réception suivant le cas (voir le manuel de la primitive readv).

Les champs msg_control et msg_controllen spécifient le tableau des données auxiliaires reçues ou émises, le champ msg_control pouvant être le pointeur NULL s'il n'y a aucune donnée auxiliaire à émettre ou recevoir. Chaque donnée auxiliaire se présente sous la forme d'une structure de type struct cmsghdr définie (dans sys/socket.h) :

struct cmsghdr {
   socklen_t cmsg_len; /* longueur en octet, en-tête inclus */
   int cmsg_level;     /* protocole (IPPROTO_IPV6, ...) */
   int cmsg_type;      /* sous-type dans le protocole (IPV6_RTHDR, ...) */
                       /* suivi par unsigned char cmsg_data[]; */
};

En raison de problèmes d'alignement (cf. figure Structure des données auxiliaires), l'accès au tableau des données auxiliaires ainsi que la manipulation de ces dernières ne doivent se faire qu'au moyen de cinq macros appropriées, définies dans <sys/socket.h> :

CS196.gif

  • struct cmsghdr *CMSG_FIRSTHDR(const struct msghdr *msgh);
    CMSG_FIRSTHDR renvoie un pointeur vers la première donnée auxiliaire contenue dans la structure de type struct msghdr pointée par msgh.
  • struct cmsghdr *CMSG_NXTHDR(const struct msghdr *msgh, const struct cmsghdr *cmsg);
    CMSG_NXTHDR renvoie un pointeur vers la donnée auxiliaire qui suit celle pointée par cmsg ou le pointeur NULL s'il n'y en a pas. Si cmsg est le pointeur NULL, CMSG_NXTHDR renvoie un pointeur vers la première donnée auxiliaire. Ainsi, CMSG_NXTHDR(msgh, NULL) est équivalent à CMSG_FIRSTHDR(msgh).
  • socklen_t CMSG_SPACE(socklen_t length);
    CMSG_SPACE renvoie le nombre d'octets occupés par une donnée auxiliaire dont la taille des données transmises est length, tout en tenant compte des alignements.
  • socklen_t CMSG_LEN(socklen_t length);
    CMSG_LEN retourne la valeur à stocker dans le champ cmsg_len de la structure (de type struct cmsghdr) associée à une donnée auxiliaire dont la taille des données transmises est length, ceci en tenant compte des alignements.
  • unsigned char *CMSG_DATA(const struct cmsghdr *cmsg);
    CMSG_DATA retourne un pointeur vers les données contenues dans la donnée auxiliaire pointée par le paramètre cmsg.

Le dernier champ msg_flags de la structure msghdr est rempli au retour de recvmsg(). Plusieurs drapeaux peuvent avoir été levés dont le drapeau MSG_TRUNC pour indiquer que les données ont été tronquées ou le drapeau MSG_CTRUNC pour indiquer que les données auxiliaires ont été tronquées.

Afin de recevoir toute donnée auxiliaire sur une socket, il faut auparavant le demander en positionnant l'option correspondante. Plus précisément, le RFC 3542 liste de manière exhaustive des options disponibles et comment les positionner :

int on = 1;
/* interface de réception / adresse destination */
setsockopt(s, IPPROTO_IPV6, IPV6_RECVPKTINFO, &on, sizeof(on));
/* nombre de sauts */
setsockopt(s, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, &on, sizeof(on));
/* en-tête de routage */
setsockopt(s, IPPROTO_IPV6, IPV6_RECVRTHDR, &on, sizeof(on));
/* options proche-en-proche */
setsockopt(s, IPPROTO_IPV6, IPV6_RECVHOPOPTS, &on, sizeof(on));
/* option destination */
setsockopt(s, IPPROTO_IPV6, IPV6_RECVDSTOPTS, &on, sizeof(on));
/* classe de trafic */
setsockopt(s, IPPROTO_IPV6, IPV6_RECVTCLASS, &on, sizeof(on));

En ce qui concerne l'émission d'une donnée auxiliaire, deux possibilités s'offrent au programmeur :

  • soit il fait appel à la primitive setsockopt pour positionner l'option correspondante avec les données adéquates. Ce sont alors des options dites permanentes (sticky) car elles s'appliquent à tous les paquets transmis par la suite et ce jusqu'à un nouvel appel à setsockopt ou une surcharge par une donnée auxiliaire.
  • soit il utilise sendmsg et les données auxiliaires affectent uniquement le datagramme concerné (non applicable au socket de type SOCK_STREAM)

Le tableau Options de données auxiliaires en émission, extrait du RFC 3542, donne la liste des options disponibles en émission (avec leur type de données associées) :

Options de données auxiliaires en émission
opt level / cmsg_level optname / cmsg_type optval / cmsg_data[]
IPPROTO_IPV6 IPV6_PKTINFO structure in6_pktinfo
IPPROTO_IPV6 IPV6_HOPLIMIT int
IPPROTO_IPV6 IPV6_NEXTHOP structure sockaddr_in6
IPPROTO_IPV6 IPV6_RTHDR structure ip6_rthdr
IPPROTO_IPV6 IPV6_HOPOPTS (prochain saut / next hop) structure ip6_hbh
IPPROTO_IPV6 IPV6_DSTOPTS structure ip6_dest
IPPROTO_IPV6 IPV6_RTHDRDSTOPTS structure ip6_dest
IPPROTO_IPV6 IPV6_TCLASS int

Les options proposées par cette API avancée, ne seront pas toutes détaillées dans ce chapitre. Nous recommandons au lecteur intéressé de se reporter au RFC 3542. L'exemple simple qui suit, met en oeuvre ces notions. Il s'agit d'un extrait de programme qui, outre les données habituelles, souhaite recevoir également deux données auxiliaires :

  • index de l'interface de réception du paquet / adresse destination du paquet reçu (option IPV6_RECVPKTINFO) et
  • le nombre de sauts (hop limit) du paquet reçu (option IPV6_RECVHOPLIMIT).

La première partie de ce programme, où les variables sont déclarées et initialisées ne présente aucune difficulté. On notera l'usage de la macro CMSG_SPACE afin d'initialiser la variable cmsg_buf destinée à accueillir les données auxiliaires demandées.

int noc, o = 1, s;
struct sockaddr_storage from;
char buf[1024];
struct iovec iov = {buf, sizeof(buf)};
char cmsg_buf[CMSG_SPACE(sizeof(struct in6_pktinfo)) + CMSG_SPACE(sizeof(int))];
struct cmsghdr *cmsg;
struct msghdr msg = {(void *) &from, sizeof(from), &iov, 1,
                     (void *) cmsg_buf, sizeof(cmsg_buf), 0};

Actuellement, un grand nombre d'implémentations ne sont pas à jour du RFC 3542 (bien que publié en mai 2003). En particulier, certaines implémentations ne distinguent toujours pas entre les options de réception et les options d'émission. Si bien qu'il peut être nécessaire d'ajouter les lignes suivantes :

#ifndef IPV6_RECVHOPLIMIT
#define IPV6_RECVHOPLIMIT IPV6_HOPLIMIT
#endif
#ifndef IPV6_RECVPKTINFO
#define IPV6_RECVPKTINFO IPV6_PKTINFO
#endif

Ensuite on indique par l'intermédiaire de la primitive setsockopt que les données auxiliaires mentionnées plus haut doivent être reçues. La variable s est un descripteur d'entrées/sorties associé à une socket PF_INET6.

if (setsockopt(s, IPPROTO_IPV6, IPV6_RECVPKTINFO, &o, sizeof(o)) ||
   setsockopt(s, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, &o, sizeof(o))) {
     /* traitement de l'erreur */
}

La primitive recvmsg est exécutée et les erreurs éventuelles sont traitées :

if ((noc = recvmsg(s, &msg, 0)) < 0) {
/* traitement de l'erreur */
}
if (msg.msg_flags & MSG_TRUNC) {
/* traitement de l'erreur (les données sont tronquées) */
}
if (msg.msg_flags & MSG_CTRUNC) {
/* traitement de l'erreur (les données auxiliaires sont tronquées) */
}

Finalement, au moyen des macros CMSG_FIRSTHDR et CMSG_NXTHDR précédemment décrites, une boucle traite les données auxiliaires reçues :

for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
   if ((cmsg->cmsg_level == IPPROTO_IPV6) && (cmsg->cmsg_type == IPV6_PKTINFO)) {
      struct in6_pktinfo *pi = (struct in6_pktinfo *) CMSG_DATA(cmsg);
      /* suite du traitement */
   }
   if ((cmsg->cmsg_level == IPPROTO_IPV6) && (cmsg->cmsg_type == IPV6_HOPLIMIT)) {
       int hlim = *(int *)CMSG_DATA(cmsg);
       /* suite du traitement */
   }
/* suite du programme */
}

L'exemple « mini-ping » revisité

Le programme one_ping6.c va être repris afin de lui ajouter deux fonctionnalités dont l'implémentation s'appuiera sur l'usage de données auxiliaires. On souhaite d'une part afficher le nombre de sauts (hop limit) du paquet ECHO_REPLY (éventuellement) reçu et d'autre part de permettre, à l'instar de la commande ping6, de passer une liste de relais par lesquels le paquet ECHO_REQUEST devra transiter avant d'être envoyé à l'hôte destinataire (routage par la source).

Par exemple, pour envoyer un paquet ECHO_REQUEST à la machine ipv6.imag.fr tout en transitant tout d'abord par les machines www.kame.net et relai.imag.fr, la commande xapi_ping6 sera :

$ xapi_ping6 www.kame.net relais.imag.fr ipv6.imag.fr
Sending ECHO REQUEST to: ipv6.imag.fr via:
www.kame.net
relais.imag.fr
Waiting for answer (timeout = 5s)...
Got answer from 2001:660:9510:25::632 (seq = 0, hoplimit = 241)

L'affichage du nombre de sauts a déjà été en grande partie traité dans l'exemple du paragraphe consacré à l'implémentation de l'API avancée. Nous indiquerons donc seulement les changements significatifs par rapport à la version originale. Ces changements concernent essentiellement la routine wait_for_echo_reply6. La première tâche à effectuer est, comme dans l'exemple précédent, de positionner l'option IPV6_RECVHOPLIMIT, juste après avoir mis en place le filtrage ICMPv6. L'instruction :

noc = recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr *) from, &from_len);

est remplacée par la nouvelle instruction :

noc = recv_data(sock, buf, sizeof(buf), 0, (struct sockaddr *) from, &from_len, &hoplimit);

hoplimit est un entier qui a été précédemment déclaré dans le corps de la fonction wait_for_echo_reply6 et recv_data a pour texte :


 1| #ifdef sun /* For Solaris */
 2| #define _XOPEN_SOURCE 500 /* correct recvmsg/sendmsg/msg/CMSG_xx syntax */
 3| #define __EXTENSIONS__
 4| #endif
 5| #include <stdio.h>
 6| #include <stdlib.h>
 7| #include <unistd.h>
 8| #include <sys/uio.h>
 9| #include <sys/types.h>
10| #include <sys/socket.h>
11| #include <netinet/in.h>
12| #include <netinet/ip6.h>
13| #ifndef CMSG_SPACE /* Solaris <= 9 */
14| #define CMSG_SPACE(l) ((size_t)_CMSG_HDR_ALIGN(sizeof (struct cmsghdr) + (l)))
15| #define CMSG_LEN(l) ((size_t)_CMSG_DATA_ALIGN(sizeof (struct cmsghdr)) + (l))
16| #endif
17|  
18| int recv_data(int sock, void *buf, int len, unsigned int flags,
19|               struct sockaddr *from, socklen_t *fromlen, int *hoplimit)
20| {
21| int ret, found = 0, cmsgspace = CMSG_SPACE(sizeof(int));
22| struct iovec iov = {buf, len};
23| struct cmsghdr *cmsg = (struct cmsghdr *) malloc(cmsgspace), *ptr;
24| struct msghdr msg = {
25|    (caddr_t) from, *fromlen, &iov, 1,
26|    (caddr_t) cmsg, cmsgspace
27| };
28|  
29| if (cmsg == NULL) {
30|    perror("recv_data: malloc");
31|    return -1;
32| }
33| ret = recvmsg(sock, &msg, flags);
34| if (ret < 0) {
35|    perror("recv_data: recvmsg");
36|    goto done;
37| }
38| if (msg.msg_flags & MSG_TRUNC) {
39|    fprintf(stderr, "recv_data: recvmsg: data discarded before delivery\n");
40|    goto bad;
41| }
42| if (msg.msg_flags & MSG_CTRUNC) {
43|    fprintf(stderr,
44|            "recv_data: recvmsg: control data lost before delivery\n");
45|    goto bad;
46| }
47| if (msg.msg_controllen)
48|    for (ptr = CMSG_FIRSTHDR(&msg); ptr; ptr = CMSG_NXTHDR(&msg, ptr)) {
49|       if (ptr->cmsg_level==IPPROTO_IPV6 && ptr->cmsg_type==IPV6_HOPLIMIT) {
50|          if (ptr->cmsg_len != CMSG_LEN(sizeof(int))) {
51|             fprintf(stderr,
52|                     "recvmsg: ancillary data with invalid length\n");
53|             goto bad;
54|          }
55|          *hoplimit = *((int *) CMSG_DATA(ptr));
56|          goto done;
57|       }
58|    }
59|    fprintf(stderr,
60|            "recv_data: recvmsg: hoplimit not found in ancillary data\n");
61|  bad:
62|    ret = -1;
63|  done:
64|    free(cmsg);
65|    return ret;
66| }

Le code de la fonction recv_data ne sera pas commenté car la réception du nombre de sauts via une donnée auxiliaire a été étudiée dans un précédent exemple, la seule différence étant que la gestion des erreurs est ici plus détaillée.

Il faut enfin modifier trivialement le code de la routine recv_icmp_pkt afin que celle-ci imprime le nombre de sauts du paquet ECHO_REPLY (éventuellement) reçu. Nous en laissons le soin au lecteur.

Pour l'autre donnée auxiliaire (cette fois ci en émission), à savoir le routage par la source, il faut naturellement tout d'abord modifier la fonction send_echo_request6 et en premier lieu son prototype qui devient :

int send_echo_request6(int sock, struct sockaddr_in6 *dst, uint16_t id,
                       uint16_t seq, char *opt_data, int opt_data_size,
                       struct in6_addr *seg, int nseg)

La routine send_echo_request6 modifié possède deux arguments supplémentaires ajoutés à la fin. Le premier de ces nouveaux arguments est un pointeur vers un tableau contenant les adresses IPv6 des relais par lesquels on souhaite effectuer le routage par la source. Le dernier argument est le nombre d'éléments de ce tableau, c'est-à-dire le nombre de relais.

Il faut ensuite substituer dans le corps de la fonction send_echo_request6 l'instruction suivante :

noc = sendto(sock, (char *) icmp, icmp_pkt_size, 0, (struct sockaddr *) dst,

par :

noc = send_data(sock, (void *) icmp, icmp_pkt_size, 0, 
                (struct sockaddr *) dst, sizeof(struct sockaddr_in6),
                seg, nseg);

Si la variable seg est le pointeur NULL, la liste des relais est vide. On fait appel à la fonction send_data, dont le code va être commenté en détails :


 1| #ifdef sun /* For Solaris */
 2| #define _XOPEN_SOURCE 500 /* correct recvmsg/sendmsg/msg/CMSG_xx syntax */
 3| #define __EXTENSIONS__
 4| #endif
 5| #include <stdio.h>
 6| #include <stdlib.h>
 7| #include <unistd.h>
 8| #include <sys/uio.h>
 9| #include <sys/types.h>
10| #include <sys/socket.h>
11| #include <netinet/in.h>
12| #include <netinet/ip6.h>
13| #ifndef CMSG_SPACE /* Solaris <= 9 */
14| #define CMSG_SPACE(l) ((size_t)_CMSG_HDR_ALIGN(sizeof (struct cmsghdr) + (l)))
15| #define CMSG_LEN(l) ((size_t)_CMSG_DATA_ALIGN(sizeof (struct cmsghdr)) + (l))
16| #endif
17| #ifndef IPV6_RECVHOPLIMIT
18| #define IPV6_RECVHOPLIMIT IPV6_HOPLIMIT
19| #endif
20|  
21| extern void * inet6_rth_init(); /* sometimes not in ip6.h */
22| 
23| int send_data(int sock, void *buf, int len, unsigned int flags,
24|               struct sockaddr *to, socklen_t tolen,
25|               struct in6_addr *seg, int nseg)
26| {
27| int ret = -1, rthsp, cmsgspace;
28| void *data;
29| struct in6_addr *in6;
30| struct iovec iov = {buf, len};
31| struct cmsghdr *cmsg = NULL;
32| struct msghdr msg = {
33|                     (caddr_t) to, tolen, &iov, 1,
34|                     NULL, 0, 0
35|                     };
36|  
37|    if (seg != NULL) {
38|       rthsp = inet6_rth_space(IPV6_RTHDR_TYPE_0, nseg);
39|       cmsgspace = CMSG_SPACE(rthsp);
40|       msg.msg_control = cmsg = (struct cmsghdr *) malloc(cmsgspace);
41|       if (cmsg == NULL) {
42|          perror("recv_data: malloc");
43|          goto bad;
44|       }
45|       cmsg->cmsg_level = IPPROTO_IPV6;
46|       msg.msg_controllen = cmsg->cmsg_len = CMSG_LEN(rthsp);
47|       cmsg->cmsg_type = IPV6_RTHDR;
48|       data = CMSG_DATA(cmsg);
49|       data = (void *)inet6_rth_init(data, rthsp, IPV6_RTHDR_TYPE_0, nseg);
50|       if (!data) {
51|          fprintf(stderr, "send_data: inet6_rth_init failed\n");
52|          goto bad;
53|       }
54|       for (in6 = seg; in6 - seg < nseg; in6++)
55|          if (inet6_rth_add(data, in6) == -1) {
56|             fprintf(stderr, "send_data: inet6_rth_add failed\n");
57|             goto bad;
58|          }
59|    }
60|    ret = sendmsg(sock, &msg, flags);
61|    if (ret < 0) {
62|       perror("send_data: sendmsg");
63|       goto bad;
64|    }
65| bad:
66|    if (cmsg)
67|    free(cmsg);
68|    return ret;
69| }

Les six premiers paramètres de la fonction send_data sont identiques à ceux de la primitive système sendto, les deux derniers étant quant à eux identiques aux deux derniers arguments de la routine send_echo_request6.

Si la liste de relais est vide, on appelle sendmsg sans données auxiliaires (msg.msg_control est nul). Sinon on alloue (ligne 40) un tampon pour contenir les données auxiliaires.

La routine inet6_rth_space est l'une des six nouvelles routines proposées par l'API avancée afin de faciliter la tâche du programmeur lors de la manipulation des en-têtes de routage. Elle prend en arguments le type de l'extension de routage (en l'occurrence la constante IPV6_RTHDR_TYPE_0 dont la valeur numérique est 0 est qui est définie dans <netinet/in.h>) et le nombre de relais contenus dans cette extension (pour ce type d'extension, ce nombre doit être compris entre 0 et 127 inclus). Elle retourne la taille en octets nécessaire pour contenir cette en-tête de routage. Ici cette routine va permettre d'initialiser, via la variable rthsp et à l'aide de la macro CMSG_SPACE, la variable cmsgspace à la taille en octets de la donnée auxiliaire associée à cette extension de routage.

En lignes 45 à 47, la longueur des données auxiliaires et la structure cmsg sont initialisés au moyen de la macro CMSG_LEN pour le champ cmsg_len.

Il faut maintenant initialiser les données transmises par la donnée auxiliaire avec l'en-tête routage (lignes 48 à 53). Nous allons nous servir de la routine inet6_rth_init fournie par l'API avancée. Celle-ci prend en premier argument un pointeur vers la zone mémoire qui contiendra l'en-tête de routage, le deuxième argument étant la taille en octets de cette zone mémoire. Les deux derniers arguments sont identiques à ceux de la routine inet6_rth_space. inet6_rth_init retourne un pointeur vers cette zone mémoire ou le pointeur NULL si la taille de celle-ci est insuffisante.

Après ces diverses initialisations, la donnée auxiliaire est représentée à la figure Initialisation de l'en-tête de routage où l'on a supposé, afin de fixer les idées, que l'on est en présence d'une architecture 32 bits et que l'alignement se fait sur 32 bits également (autrement dit il n'y a pas de bourrage entre la structure cmsg et le début des données transmises, cf. figure Structure des données auxiliaires).

CS197.gif

Dans la boucle qui suit (lignes 54 à 58), l'initialisation de l'en-tête de routage se termine en ajoutant successivement les adresses IPv6 des relais du routage par la source. Ces ajouts se font au moyen de la fonction inet6_rth_add qui prend en premier argument la zone mémoire contenant l'en-tête de routage et en deuxième argument un pointeur (de type struct in6_addr *) vers l'adresse du relais à ajouter.

A l'issue de cette boucle, si l'on reprend l'exemple qui nous a servi à présenter la nouvelle version de la commande one_ping6 :

$ xapi_ping6 www.kame.net relais.imag.fr ipv6.imag.fr

la donnée auxiliaire sera maintenant comme représentée à la figure Adjonction des deux relais dans l'en-tête de routage, (avec les mêmes hypothèses sur l'architecture et l'alignement). Le message ainsi construit est expédié tout en gérant les erreurs éventuelles (nous laissons le soin au lecteur l'adaptation de la fonction main afin de prendre en compte les nouveaux arguments (optionnels) du programme one_ping6).

CS198.gif

On remarque que la donnée auxiliaire contient les adresses des relais intermédiaires, alors que dans un paquet IPv6, l'en-tête de routage contient les adresses à partir du deuxième relais et l'adresse destination finale, l'adresse du premier relais étant dans l'en-tête IPv6. Le noyau lors du sendmsg va permuter les adresses pour rétablir l'ordre correct.

Portabilité du code

Solaris définit des prototypes de sendmsg et recvmsg variables selon les modes de compilation. De plus, jusqu'à la version 9 incluse, il ne définit pas les macros CMSG_SPACE et CMSG_LEN. Les lignes 1 à 4 et 13 à 19 du programme servent à éviter ces problèmes de compatibilité.

D'autre part, les fonctions inet6_rth_xxx, définies dans le RFC 3542 sont encore souvent absentes de la librairie système (c'est le cas pour Solaris 9, FreeBSD4.x, NetBSD1.x, et Linux). Le lecteur peut les remplacer par un codage à la main, ou récupérer leur texte, par exemple dans la distribution KAME.

Personal tools