L'exemple « mini-ping » revisité
From Livre IPv6
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);
où 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 :
#ifdef sun /* For Solaris */ #define _XOPEN_SOURCE 500 /* correct recvmsg/sendmsg/msg/CMSG_xx syntax */ #define __EXTENSIONS__ #endif #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/uio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip6.h> #ifndef CMSG_SPACE /* Solaris <= 9 */ #define CMSG_SPACE(l) ((size_t)_CMSG_HDR_ALIGN(sizeof (struct cmsghdr) + (l))) #define CMSG_LEN(l) ((size_t)_CMSG_DATA_ALIGN(sizeof (struct cmsghdr)) + (l)) #endif int recv_data(int sock, void *buf, int len, unsigned int flags, struct sockaddr *from, socklen_t *fromlen, int *hoplimit) { int ret, found = 0, cmsgspace = CMSG_SPACE(sizeof(int)); struct iovec iov = {buf, len}; struct cmsghdr *cmsg = (struct cmsghdr *) malloc(cmsgspace), *ptr; struct msghdr msg = { (caddr_t) from, *fromlen, &iov, 1, (caddr_t) cmsg, cmsgspace }; if (cmsg == NULL) { perror("recv_data: malloc"); return -1; } ret = recvmsg(sock, &msg, flags); if (ret < 0) { perror("recv_data: recvmsg"); goto done; } if (msg.msg_flags & MSG_TRUNC) { fprintf(stderr, "recv_data: recvmsg: data discarded before delivery\n"); goto bad; } if (msg.msg_flags & MSG_CTRUNC) { fprintf(stderr, "recv_data: recvmsg: control data lost before delivery\n"); goto bad; } if (msg.msg_controllen) for (ptr = CMSG_FIRSTHDR(&msg); ptr; ptr = CMSG_NXTHDR(&msg, ptr)) { if (ptr->cmsg_level==IPPROTO_IPV6 && ptr->cmsg_type==IPV6_HOPLIMIT) { if (ptr->cmsg_len != CMSG_LEN(sizeof(int))) { fprintf(stderr, "recvmsg: ancillary data with invalid length\n"); goto bad; } *hoplimit = *((int *) CMSG_DATA(ptr)); goto done; } } fprintf(stderr, "recv_data: recvmsg: hoplimit not found in ancillary data\n"); bad: ret = -1; done: free(cmsg); return ret; }
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 :
#ifdef sun /* For Solaris */ #define _XOPEN_SOURCE 500 /* correct recvmsg/sendmsg/msg/CMSG_xx syntax */ #define __EXTENSIONS__ #endif #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/uio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip6.h> #ifndef CMSG_SPACE /* Solaris <= 9 */ #define CMSG_SPACE(l) ((size_t)_CMSG_HDR_ALIGN(sizeof (struct cmsghdr) + (l))) #define CMSG_LEN(l) ((size_t)_CMSG_DATA_ALIGN(sizeof (struct cmsghdr)) + (l)) #endif #ifndef IPV6_RECVHOPLIMIT #define IPV6_RECVHOPLIMIT IPV6_HOPLIMIT #endif extern void * inet6_rth_init(); /* sometimes not in ip6.h */ int send_data(int sock, void *buf, int len, unsigned int flags, struct sockaddr *to, socklen_t tolen, struct in6_addr *seg, int nseg) { int ret = -1, rthsp, cmsgspace; void *data; struct in6_addr *in6; struct iovec iov = {buf, len}; struct cmsghdr *cmsg = NULL; struct msghdr msg = { (caddr_t) to, tolen, &iov, 1, NULL, 0, 0 }; if (seg != NULL) { rthsp = inet6_rth_space(IPV6_RTHDR_TYPE_0, nseg); cmsgspace = CMSG_SPACE(rthsp); msg.msg_control = cmsg = (struct cmsghdr *) malloc(cmsgspace); if (cmsg == NULL) { perror("recv_data: malloc"); goto bad; } cmsg->cmsg_level = IPPROTO_IPV6; msg.msg_controllen = cmsg->cmsg_len = CMSG_LEN(rthsp); cmsg->cmsg_type = IPV6_RTHDR; data = CMSG_DATA(cmsg); data = (void *)inet6_rth_init(data, rthsp, IPV6_RTHDR_TYPE_0, nseg); if (!data) { fprintf(stderr, "send_data: inet6_rth_init failed\n"); goto bad; } for (in6 = seg; in6 - seg < nseg; in6++) if (inet6_rth_add(data, in6) == -1) { fprintf(stderr, "send_data: inet6_rth_add failed\n"); goto bad; } } ret = sendmsg(sock, &msg, flags); if (ret < 0) { perror("send_data: sendmsg"); goto bad; } bad: if (cmsg) free(cmsg); return ret; }
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 See ) 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 See à See , 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 See à See ). 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).
Dans la boucle qui suit (lignes See à See ), 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).
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 See à See et See à See du programme (See ) 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.