Difference between revisions of "Programmation d'applications bis"

From Livre IPv6

(Deux types de serveurs)
(Deux types de serveurs)
Line 246: Line 246:
  
 
{|
 
{|
|La première solution est d'ouvrir une socket <tt>PF_INET</tt> et une socket <tt>PF_INET6</tt>, gérant respectivement le traffic IPv4 (en orange sur la figure ci-contre) et IPv6 (en bleu). Ensuite, soit on implémente un serveur qui écoute simultanément sur ces deux sockets (utilisation d'un <tt>select()</tt>), soit on implémente deux serveurs, un pour chaque socket.
+
|La première solution est d'ouvrir une socket <tt>PF_INET</tt> et une socket <tt>PF_INET6</tt>, gérant respectivement le trafic IPv4 (en orange sur la figure ci-contre) et IPv6 (en bleu). Ensuite, soit on implémente un serveur qui écoute simultanément sur ces deux sockets (utilisation d'un <tt>select()</tt>), soit on implémente deux serveurs, un pour chaque socket.
 
||<tikz title="Serveur utilisant une socket PF_INET et une socket PF_INET6">
 
||<tikz title="Serveur utilisant une socket PF_INET et une socket PF_INET6">
 
           \clip (-1, -0.5) rectangle (11.5,3);
 
           \clip (-1, -0.5) rectangle (11.5,3);
Line 269: Line 269:
  
 
|-
 
|-
|La deuxième solution est d'ouvrir une seule socket <tt>PF_INET6</tt>, 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 trafic IPv4 puisse atteindre la socket <tt>PF_INET6</tt>. 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 <tt>IPV6_V6ONLY</tt>.  
+
|La deuxième solution est d'ouvrir une seule socket <tt>PF_INET6</tt>, qui acceptera à la fois les clients IPv4 et IPv6. Dans ce cas les adresses IPv4 sont traduites en adresses "IPv4 mappées en IPv6" (trafic jaune sur la figure ci-contre) de façon à ce que le trafic IPv4 puisse atteindre la socket <tt>PF_INET6</tt>. 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 <tt>IPV6_V6ONLY</tt>.  
 
||<tikz title="Serveur utilisant une seule socket PF_INET6">
 
||<tikz title="Serveur utilisant une seule socket PF_INET6">
 
           \clip (-1, -0.5) rectangle (11.5,3);
 
           \clip (-1, -0.5) rectangle (11.5,3);

Revision as of 08:49, 27 April 2011

Contents

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.

L'interface de programmation réseau ("API") la plus connue est l'interface "socket" (dite aussi interface "BSD"). La première partie de ce chapitre présentera les modifications introduites dans cette interface de programmation pour supporter IPv6.

La partie suivante illustrera l'utilisation de l'API "avancée". Plusieurs exemples de programmes seront présentés dans ces deux premières parties.

Une troisième partie fera un état des lieux du support d'IPv6 dans les langages de programmation les plus communs.

La quatrième partie sera consacrée aux méthodes de test permettant de savoir si un programme donné est compatible IPv6 ou non.

La dernière partie présentera une méthode pour rendre les programmes compatible IPv6 de manière automatique (sans modification du code source).

Gestion d'IPv6 avec l'API socket classique

Généralités

Les changements opérés dans l'API de façon à intégrer IPv6 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 plus loin.

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. Elle est disponible dans de nombreux environnements de programmation tels que Java, perl, python, ruby.

Le langage choisi dans ce chapitre pour décrire les constantes, fonctions et structures est le langage C, sauf en ce qui concerne les exemples, pour lesquels plusieurs langages sont illustrés. On peut cependant noter que la syntaxe nécessaire pour utiliser cette API est similaire dans beaucoup de langages.

Liste des changements

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

Nouvelles 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

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.

Nouvelle 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.

Nouvelle 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.

Nouvelles 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.

Nouvelles fonctions de conversion entre adresse binaire et alphanumérique

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.

Il est préférable d'éviter ces fonctions et de leur préférer les fonctions getaddrinfo et getnameinfo, avec les flags AI_NUMERICHOST et NI_NUMERICHOST, de façon à obtenir un code indépendant de la famille d'adresse (voir le paragraphe consacré à ce sujet).

#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.

Modifications et nouvelles possibilités concernant l'ICMP

Beaucoup de changements sont apparus entre l'ICMP (IPv4) et l'ICMPv6, comme on a pu le voir dans les mécanismes intervenant en réseau local (auto-configuration, etc.). Certains changements modifient également la manière de programmer. Ceux-ci étant assez difficile à décrire en restant sur un plan théorique, nous les aborderons au cours des exemples ("ping", "multicast") donnés plus loin.

Changements impliquant un travail de migration important

Même si la liste précédente peut sembler assez longue, la plupart des changements sont évidents ou concernent une petite minorité de programmes (ceux travaillant à bas niveau avec les primitives réseau). En réalité, seulement deux de ces changements impliquent souvent un travail de migration important :

  • La nouvelle primitive de résolution de noms getaddrinfo() 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 utilisant des structures adaptées comme struct sockaddr_storage.

Ce qui ne change pas

PF_INET6 et AF_INET6

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.

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);


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()

Programmation sur un système double-pile

Les systèmes d'exploitation actuels implémentent une double-pile IPv4 / IPv6. Cette partie détaille les possibilités offertes et les implications qui découlent de cette architecture.

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 trafic IPv4 (en orange sur la figure ci-contre) et IPv6 (en bleu). Ensuite, soit on implémente un serveur qui écoute simultanément sur ces deux sockets (utilisation d'un select()), soit on implémente deux serveurs, un pour chaque socket. 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" (trafic jaune sur la figure ci-contre) de façon à ce que le trafic 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

Ce deuxième type de serveur peut paraître plus simple à réaliser que le précédent (pas besoin de l'étape select() car on n'a qu'une seule socket). Attention cependant, en toute rigueur, à penser aux systèmes qui ne gèrent pas IPv6 (par exemple si le module IPv6 n'est pas chargé) : en effet si on passe AF_INET6 à la primitive getaddrinfo() sur ce genre de système, celle-ci ne renverra aucune adresse ; or notre programme doit, à défaut d'accepter les clients IPv6, au moins accepter les clients IPv4. Notre programme devrait donc dans ce cas refaire un appel à getaddrinfo() pour finalement tenter d'ouvrir une socket IPv4.

Les adresses IPv4 mappées

Comme expliqué ci-dessus, les adresses IPv4 mappées permettent de faire passer le traffic IPv4 dans l'espace d'adressage IPv6, et ainsi interagir avec les sockets du type PF_INET6.

Elles sont de la forme ::FFFF:XXXX:YYYYXXXXYYYY est la valeur hexadécimale correspondant à l'adresse IPv4 a.b.c.d. Cependant, pour faciliter la lecture, elles sont généralement représentées sous la forme ::FFFF:a.b.c.d.

L'option IPV6_V6ONLY

L'option IPV6_V6ONLY contrôle si une 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é dans ce cas).

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.

Ces erreurs d'implémentations sont illustrées plus loin dans l'exemple de Client/Serveur TCP.

Si l'option IPV6_V6ONLY 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.

Programmation indépendante de la famille d'adresses

Les nouvelles primitives getaddrinfo() et getnameinfo() permettent de programmer de manière indépendante de la famille d'adresse car elles gèrent en interne les différences entre les adresses IPv4 et IPv6. Le code qui les utilise peut donc utiliser des structures sockaddr et sockaddr_storage sans préciser s'il s'agit d'adresses IPv4 ou IPv6.

Cette manière de programmer est recommandée car le code associé est plus facile à maintenir. De plus, si le protocole doit à nouveau changer dans le futur, un code indépendant de la version de protocole ne nécessitera pas de nouvelle migration.

A l'inverse, le fait que les primitives inet_pton() et inet_ntop() ont un argument précisant la famille d'adresse rend généralement le code qui les utilise dépendant de la famille d'adresses (sauf si cette famille est obtenue dynamiquement, par exemple via un getaddrinfo()). Il est donc généralement préferable d'utiliser les fonctions getaddrinfo() et getnameinfo(), avec le flag AI_NUMERICHOST, plutôt que inet_pton() et inet_ntop().

De même, on évitera dans la mesure du possible d'utiliser des structures spécifiques à IPv6 ou à IPv4.

Sans compter la gestion de l'option de socket IPV6_V6ONLY, les seuls programmes qui nécessitent un code dépendant de la famille d'adresse sont ceux qui utilisent des fonctionnalités spécifiques à IPv6 (inexistantes en IPv4) ou qui exploitent des détails qui sont différents en IPv4 et en IPv6. Cela représente en fait une petite minorité. Les autres, à commencer par ceux qui sont de simple portages vers IPv6, devraient présenter de préférence un code indépendant ou très peu dépendant de la famille d'adresses.

Exemples

Résolution de noms et résolution inverse (langage C)

L'exemple proposé n'est autre qu'une sorte de nslookup (très) simplifié : la commande haah (host-address-address-host). 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
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
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>
 2 | #include <stdlib.h>
 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 | #define HOST_MAXLEN    128
12 | #define IP_MAXLEN    INET6_ADDRSTRLEN
13 |
14 | int main(int argc, char **argv)
15 | {
16 |     int ret;
17 |     char host[HOST_MAXLEN];
18 |     char ip[IP_MAXLEN];
19 |     struct addrinfo *res, *ptr;
20 |     struct addrinfo hints = {
21 |         0,
22 |         PF_UNSPEC,
23 |         SOCK_STREAM,
24 |         0,
25 |         0,
26 |         NULL,
27 |         NULL,
28 |         NULL
29 |     };
30 |
31 |     if (argc != 2) {
32 |         fprintf(stderr, "%s: usage: %s host | addr.\n", *argv, *argv);
33 |         exit(1);
34 |     }
35 |
36 |     // getaddrinfo will give us the list of all addresses corresponding to argv[1]
37 |     ret = getaddrinfo(argv[1], NULL, &hints, &res);
38 |     if (ret) {
39 |         fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret));
40 |         exit(1);
41 |     }
42 |
43 |     if (res != NULL)
44 |     {
45 |         // let's retrieve the hostname by using getnameinfo on the first address of the list
46 |         ret = getnameinfo(res->ai_addr, res->ai_addrlen,
47 |                 host, HOST_MAXLEN,
48 |                 NULL, 0, 0);
49 |         if (ret) {
50 |             fprintf(stderr, "getnameinfo: %s\n", gai_strerror(ret));
51 |             exit(1);
52 |         }
53 |         fprintf(stdout,"Name:\n%s\nAdresses:\n", host);
54 |     }
55 |     
56 |     // for each address of the list, convert it to text using getnameinfo(..., NI_NUMERICHOST) 
57 |     for (ptr = res; ptr; ptr = ptr->ai_next) {
58 |         ret = getnameinfo(ptr->ai_addr, ptr->ai_addrlen,
59 |                 ip, IP_MAXLEN,
60 |                 NULL, 0, NI_NUMERICHOST);
61 |         if (ret) {
62 |             fprintf(stderr, "getnameinfo: %s\n", gai_strerror(ret));
63 |             exit(1);
64 |         }   
65 |         fprintf(stdout, "%s\n", ip);
66 |     }   
67 |
68 |     freeaddrinfo(res);
69 |     exit(0);
70 | }

Client/Serveur TCP (langage C)

Vue d'ensemble

Le client/serveur choisi est particulièrement simple de façon à privilégier l'aspect réseau dans la présentation. Le serveur écrit un message de bienvenue à chaque client qui se connecte puis coupe la connexion. Le client est conçu de manière à tester ce serveur : il essaie de se connecter en IPv4 et, en cas de succès, il lit et affiche le message envoyé par le serveur ; ensuite il réitère ces opérations en utilisant IPv6.

Le serveur prend en paramètre le port TCP d'écoute. Le client prend en paramètre le hostname du serveur et ce port d'écoute.

Cet exemple va aussi nous permettre d'expérimenter la cohabitation entre les sockets serveur IPv4 et IPv6. Pour cela, le serveur pose quelques questions à l'utilisateur afin de déterminer le fonctionnement à adopter, comme nous allons le voir dans les sections suivantes.

Implémentations correctes d'un serveur

Lançons et paramétrons notre serveur :

$ ./server 20000
Should the server open an IPv4 socket? (y/n) y
Should the server open an IPv6 socket? (y/n) y
Should this IPv6 socket also accept IPv4 clients? (y/n) n
[Running...]

La troisième question détermine en fait la valeur (à 0 si on répond 'y', à 1 sinon) de l'option IPV6_V6ONLY attendue pour la socket IPv6. Dans ce premier exemple, on crée donc deux sockets, une socket IPv4 et une socket IPv6, avec l'option IPV6_V6ONLY positionnée à 1, ce qui correspond donc au modèle de serveur utilisant deux sockets (se référer au paragraphe "Programmation sur un système double-pile").

On peut également paramétrer le serveur suivant l'autre modèle, qui n'utilise qu'une seule socket IPv6 :

$ ./server 20000
Should the server open an IPv4 socket? (y/n) n
Should the server open an IPv6 socket? (y/n) y
Should this IPv6 socket also accept IPv4 clients? (y/n) y
[Running...]

L'option IPV6_V6ONLY étant ici positionnée à 0, la socket IPv6 acceptera aussi les clients IPv4.

Dans ces deux premiers cas, le client peut donc se connecter au serveur avec les deux versions du protocole sans soucis :

$ ./client server-host 20000
Successfully connected to server using IPv4. Server says: Hello!

Successfully connected to server using IPv6. Server says: Hello!

$
Erreurs d'implémentation courantes

Nous allons maintenant observer les deux cas d'erreur d'implémentation courants qui correspondent en fait à une option IPV6_V6ONLY mal positionnée.

Si on tente d'ouvrir une socket IPv4 et une socket IPv6 avec l'option IPV6_V6ONLY à 0, alors ces deux sockets sont susceptibles de recevoir le traffic IPv4. Ce conflit est détecté au moment de l'appel à bind() sur la deuxième socket, qui échoue donc avec l'erreur EADDRINUSE.

$ ./server 20000
Should the server open an IPv4 socket? (y/n) y
Should the server open an IPv6 socket? (y/n) y
Should this IPv6 socket also accept IPv4 clients? (y/n) y
bind() error: Address already in use
$

Tentons maintenant de créer notre serveur avec une seule socket IPv6, mais avec l'option IPV6_V6ONLY positionnée à 1.

$ ./server 20000
Should the server open an IPv4 socket? (y/n) n
Should the server open an IPv6 socket? (y/n) y
Should this IPv6 socket also accept IPv4 clients? (y/n) n
[Running...]

Aucune erreur n'est détectée au niveau du serveur. Par contre les clients IPv4 ne peuvent pas se connecter :

$ ./client server-host 20000
connect() error: Connection refused
Could not connect using IPv4!
Successfully connected to server using IPv6. Server says: Hello!

$

Il faut garder à l'esprit que, dans le cas général, ces erreurs d'implémentation sont beaucoup moins évidentes. En effet, ici on indique explicitement la valeur de l'option IPV6_V6ONLY, ce qui, souvent dans ce genre de mauvaise implémentation, n'est pas fait. Or si on ne le fait pas, le fonctionnement correct ou incorrect du programme dépendra de la valeur par défaut de cette option, qui varie suivant les systèmes (se référer au paragraphe "Programmation sur un système double-pile").

Implémentation

L'implémentation se compose de quatre fichiers sources :

  • hello_client.c le fichier source principal du client
  • open_conn.c le fichier source qui gère l'ouverture d'une connexion client
  • hello_server.c le fichier source principal du serveur
  • tools.c quelques fonctions utilitaires
Le client

Note : pour garder un code simple et se focaliser sur notre problématique, on se permettra quelques libertés quant-à la portabilité du code (à l'image de la déclaration _GNU_SOURCE qui nous permet de profiter de la primitive getline(), ci-dessous).

Le client cherche à communiquer avec le serveur en IPv4, puis en IPv6. Pour chaque cas, si la connexion réussit, il lit et affiche le message envoyé par le serveur. Ce fonctionnement se retrouve dans le fichier hello_client.c :

 1 | #define _GNU_SOURCE
 2 | #include <stdio.h>
 3 | #include <stdlib.h>
 4 | #include <sys/socket.h>
 5 |
 6 | extern open_conn(char *, char *, int);
 7 |
 8 | void try_with_family(char *host, char *port, int family, char *family_label);
 9 |
10 | int main(int argc, char **argv)
11 | {
12 |   if (argc != 3) {
13 |     fprintf(stderr, "Usage: %s <host> <port>\n", argv[0]);
14 |     exit(1);
15 |   }
16 |
17 |   // try to connect to the server using IPv4 and IPv6
18 |   try_with_family(argv[1], argv[2], AF_INET, "IPv4");
19 |   try_with_family(argv[1], argv[2], AF_INET6, "IPv6");
20 |
21 |   return 0;
22 | }
23 |
24 | void try_with_family(char *host, char *port, int family, char *family_label)
25 | {
26 |   int sock;
27 |   FILE *socket_filedesc;
28 |   char *server_message = NULL;
29 |   size_t server_message_size = 0;
30 |
31 |   if ((sock = open_conn(host, port, family)) < 0) {
32 |     // failed
33 |     fprintf(stderr, "Could not connect using %s!\n", family_label);
34 |   }
35 |   else
36 |   {  // succeeded
37 |     printf("Successfully connected to server using %s. ", family_label);
38 |
39 |     // read and display the server message
40 |     socket_filedesc = fdopen(sock, "r");
41 |     getline(&server_message, &server_message_size, socket_filedesc);
42 |
43 |     if (server_message_size > 0) {
44 |       printf("Server says: %s\n", server_message);
45 |     }
46 |   }
47 | }

La connexion en elle-même est implémentée dans la fonction open_conn() (fichier open_conn.c) :

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

Cette fonction retourne soit -1 en cas d'erreur, soit le descripteur associé à la socket de connexion. Dans notre exemple, l'utilité du client est de pouvoir tester notre serveur, on veut donc maitriser le protocole utilisé lors de la connexion. La partie résolution de nom utilise donc la primitive getaddrinfo() avec le champ ai_family de la structure hints positionné à la version du protocole que l'on veut tester (PF_INET ou PF_INET6). Pour simplifier on ne teste ici que la première adresse renvoyée. (Il peut exister des cas ou le serveur possède plusieurs adresses, pour une même version du protocole, on devrait donc en théorie implémenter une boucle.)

Adaptation du client à un cas plus général

Dans le cas d'un client TCP plus usuel, on initialiserait ai_family à PF_UNSPEC, de façon à obtenir toutes les adresses du serveur indépendamment de la version du protocole. Ensuite on bouclerait sur ces adresses jusqu'à ce que l'une d'elles fournisse une connexion valide.

Le serveur

Un serveur TCP suit généralement le processus suivant :

CS194.gif

Dans notre cas, pour simplifier, on implémentera le serveur dans un seul processus (pas de primitive fork() ni de gestion des processus fils).

Voici le code qui le décrit (hello_server.c) :

  1 | #include <stdio.h>
  2 | #include <stdlib.h>
  3 | #include <string.h>
  4 | #include <sys/socket.h>
  5 | #include <netdb.h>
  6 |
  7 | #define SERVER_MESSAGE "Hello!"
  8 |
  9 | extern char *get_family_label(int family);
 10 | extern int test_yes_or_no();
 11 |
 12 | int main(int argc, char **argv)
 13 | {
 14 |   int sock[2], ecode, num_sock = 0, on = 1, off = 0,
 15 |       communication_sock, len, sock_index, max_fd = -1;
 16 |   struct addrinfo *res, *rres, hints;
 17 |   struct sockaddr_storage from;
 18 |   fd_set fdset;
 19 |   FILE *socket_filedesc;
 20 |
 21 |   if (argc != 2) {
 22 |     fprintf(stderr, "Usage: %s <port>\n", argv[0]);
 23 |     exit(1);
 24 |   }
 25 |
 26 |   // use getaddrinfo to get information about the server sockets we may create
 27 |   memset(&hints, 0, sizeof hints) ;
 28 |   hints.ai_flags = AI_PASSIVE;
 29 |   hints.ai_socktype = SOCK_STREAM;
 30 |   hints.ai_family = AF_UNSPEC;
 31 |   ecode = getaddrinfo(NULL, argv[1], &hints, &rres);
 32 |   if (ecode) {
 33 |     fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ecode));
 34 |     exit(1);
 35 |   }
 36 |
 37 |   // for each of these possible server sockets
 38 |   for (res = rres; res; res = res->ai_next) {
 39 |     // ask the user whether we should create a server socket of this family
 40 |     printf("Should the server open an %s socket? (y/n) ", get_family_label(res->ai_family));
 41 |     if (test_yes_or_no()) {
 42 |       // if yes then create this socket
 43 |       sock[num_sock] = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
 44 |       if (sock[num_sock] < 0) {
 45 |         perror("socket() error");
 46 |         continue;
 47 |       }
 48 |       // set the SO_REUSEADDR option
 49 |       // (this will avoid EADDRINUSE errors if we restart the server too quickly)
 50 |       setsockopt(sock[num_sock], SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on));
 51 |
 52 |       if (res->ai_family == AF_INET6) {
 53 |         // ask the user whether we should set IPV6_V6ONLY to 0 or 1
 54 |         // (i.e. enable or disable IPv4 mapping into IPv6)
 55 |         printf("Should this IPv6 socket also accept IPv4 clients? (y/n) ");
 56 |         if (test_yes_or_no())
 57 |         {
 58 |           setsockopt(sock[num_sock], IPPROTO_IPV6, IPV6_V6ONLY, (char *)&off, sizeof(off));
 59 |         }
 60 |         else
 61 |         {
 62 |           setsockopt(sock[num_sock], IPPROTO_IPV6, IPV6_V6ONLY, (char *)&on, sizeof(on));
 63 |         }
 64 |       }
 65 |       // bind the socket
 66 |       if (bind(sock[num_sock], res->ai_addr, res->ai_addrlen) < 0) {
 67 |         perror("bind() error");
 68 |         exit(1);
 69 |       }
 70 |       listen(sock[num_sock], SOMAXCONN);
 71 |       num_sock++;
 72 |     }
 73 |   }
 74 |   // free memory
 75 |   freeaddrinfo(rres);
 76 |
 77 |   printf("[Running...]\n");
 78 |
 79 |   for (;;) {  // infinite loop
 80 |
 81 |     // prepare fdset for the select() below
 82 |     FD_ZERO(&fdset);
 83 |     for (sock_index = 0; sock_index < num_sock; sock_index++) {
 84 |       if (max_fd < sock[sock_index])
 85 |         max_fd = sock[sock_index];
 86 |       FD_SET(sock[sock_index], &fdset);
 87 |     }
 88 |
 89 |     // wait on all sockets
 90 |     select(max_fd+1, &fdset, NULL, NULL, NULL);
 91 |
 92 |     // manage each client connection
 93 |     for (sock_index = 0; sock_index < num_sock; sock_index++) {
 94 |       if (FD_ISSET(sock[sock_index], &fdset)) {
 95 |         len = sizeof from;
 96 |         // accept the client
 97 |         communication_sock = accept(sock[sock_index], (struct sockaddr *)&from, &len);
 98 |         // write a message to the client
 99 |         socket_filedesc = fdopen(communication_sock, "w+");
100 |         fprintf(socket_filedesc, "%s\n", SERVER_MESSAGE);
101 |         fflush(socket_filedesc);
102 |         // close the connection
103 |         close(communication_sock);
104 |       }
105 |     }
106 |   }
107 |
108 |   return 0;
109 | }

L'étape principale concernant la gestion d'IPv6 consiste en la préparation de l'adresse de la socket d'écoute du serveur en faisant appel, comme d'habitude, à la primitive getaddrinfo() (et, plus loin, freeaddrinfo() pour libérer la mémoire). Comme il s'agit d'une socket d'écoute, le champ ai_flags de la structure hints a été initialisé à la valeur AI_PASSIVE. Le champ ai_family de cette même structure a lui été initialisé à la valeur AF_UNSPEC (récupération de toutes les adresses d'écoute possibles).

Dans ce cas, s'agissant d'une socket d'écoute (AI_PASSIVE) les adresses obtenues en sortie de getaddrinfo() seront en fait les adresses "wildcard" IPv4 et IPv6. L'ordre de ces adresses dans la liste renvoyée dépend du système d'exploitation.

Ensuite (lignes 37 à 73), on boucle sur cette liste et, suivant les instructions de l'utilisateur, on crée ou non une socket d'écoute de la famille indiquée.

Une autre spécificité de la gestion d'IPv6 est l'indication de la valeur de l'option IPV6_V6ONLY (pour les sockets IPv6 uniquement), qui doit être effectuée entre la primitive socket() et la primitive bind(), et qui est, pour notre exemple, également soumise à l'utilisateur.

Une fois ces initialisations effectuées, le serveur est prêt pour boucler (lignes 79 à 106) en écoutant simultanément sur toutes ces sockets (via la primitive select()) et gérant chaque client qui s'y connecte.

Adaptation du serveur à un cas plus général

Pour implémenter un serveur TCP capable de recevoir à la fois des connexions IPv4 et IPv6, le lecteur peut donc se baser sur cet exemple simple. Il doit alors choisir un des deux modèles de serveurs (une socket IPv6 ou deux sockets), puis adapter le code source hello_server.c en conséquence, en supprimant les questions à l'utilisateur, c'est à dire :

  • pour un serveur utilisant deux sockets (IPv4 et IPv6) :
    • supprimer les lignes 39 à 42 et 72 : pas de demande à l'utilisateur concernant la création de chaque socket
    • supprimer les lignes 53 à 61 et 63 de façon à ce que l'option IPV6_V6ONLY soit positionnée à 1 sur la socket IPv6
  • pour un serveur utilisant une seule socket IPv6 :
    • remplacer AF_UNSPEC par AF_INET6 en ligne 30 (ne rechercher que les adresses IPv6) (*)
    • supprimer les lignes 39 à 42 et 72 : pas de demande à l'utilisateur concernant la creation de chaque socket
    • supprimer les lignes 53 à 57 et 59 à 63 de façon à ce que l'option IPV6_V6ONLY soit positionnée à 0

(*) En fait cette adaptation n'est pas parfaite : si le serveur TCP est installé sur une machine ne gérant pas IPv6, il ne fonctionnera pas correctement. En comparaison, l'autre type de serveur (avec deux sockets) ouvrirait bien une socket IPv4. Pour éviter cette imperfection, on pourrait vérifier si l'appel getaddrinfo() (en ligne 31) renvoie au moins une adresse, et dans le cas contraire refaire un appel en précisant cette fois hints.ai_family = AF_INET.

Le fichier tools.c

Ce fichier tools.c ne présente pas d'intérêt dans notre problématique, mais nous le donnons ici de manière à ce que notre exemple soit complet.

 1 | #include <stdio.h>
 2 | #include <sys/socket.h>
 3 | #include <string.h>
 4 |
 5 | #define MAX_ANSWER_SIZE 16
 6 |
 7 | char *get_family_label(int family)
 8 | {
 9 |   if (family == AF_INET6)
10 |     return "IPv6";
11 |   else if (family == AF_INET)
12 |     return "IPv4";
13 |   else
14 |     return "(unknown family!)";
15 | }
16 |
17 | int character_offset(char *string, char character)
18 | {
19 |   char *pointer;
20 |   pointer = strchr(string, character);
21 |   if (pointer == NULL)
22 |     return strlen(string);
23 |   else
24 |     return pointer - string;
25 | }
26 |
27 | int test_yes_or_no()
28 | {
29 |   char answer_storage[MAX_ANSWER_SIZE], *answer;
30 |   int y_offset, n_offset;
31 |   answer = fgets(answer_storage, MAX_ANSWER_SIZE, stdin);
32 |   y_offset = character_offset(answer, 'y');
33 |   n_offset = character_offset(answer, 'n');
34 |   return (y_offset < n_offset);
35 | }

Un programme du type ping (langage C)

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 (langage C)

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| }

L'API avancée

Généralités

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.

Opérations disponibles

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)

Utilisation

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.

Note : 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. Voici leurs prototypes :

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 sont pas toutes détaillées ici. Nous recommandons au lecteur intéressé de se reporter au RFC 3542.

Portabilité

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 (bien que publié en mai 2003) 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.

Enfin, 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 de définir manuellement des constantes. Par exemple :

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

L'exemple « mini-ping » revisité (langage C)

Présentation

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)


Les fonctionnalités ont déjà été en grande partie traité dans l'exemple relatif à l'API classique. Nous indiquerons donc seulement les changements significatifs par rapport à la version originale.

Principe de lecture des données auxiliaires

Outre les données habituelles, nous souhaitons 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).

Le principe consiste à indiquer 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 ensuite 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 */
}

Détail des changements à faire dans l'exemple précédent

Les changements dans le programme précédent concernent essentiellement la routine wait_for_echo_reply6. La première tâche à effectuer est de positionner l'option IPV6_RECVHOPLIMIT, juste après avoir mis en place le filtrage ICMPv6, comme indiqué dans le paragraphe précédent.

De plus, 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| }

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.

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" dans le paragraphe "Utilisation").

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.

Les langages de programmation et IPv6

Language C/C++

La partie détaillant l'API classique nous a montré comment utiliser IPv6 avec le langage C. Après comparaison avec les autres langages, il ressort les inconvénients suivants :

  • Le langage ne fournissant pas de système de gestion automatique de la mémoire, il faut modifier les programmes stockant des adresses IP (struct sockaddr -> struct sockaddr_storage).
  • Ce langage étant bas-niveau, il est très verbeux.

Il est souvent avantageux de programmer de manière plus haut-niveau, en laissant une librairie se charger des tâches bas niveau. Un exemple de librairie réseau gérant correctement IPv6 est la librairie "boost::asio" (pour plus d'information voir https://edms.cern.ch/document/935729).

Python

L'API bas-niveau IPv6 en python est très proche de l'API C. On peut remarquer l'inconvénient suivant :

  • Le symbole IPV6_V6ONLY n'est pas défini par le langage. On peut cependant définir manuellement une telle constante après avoir récupéré sa valeur dans les fichiers "headers" du langage C.

Il est également possible de programmer de manière plus "haut-niveau" en python, par exemple, dans le cas de l'écriture d'un serveur TCP, en utilisant la classe ThreadingTCPServer du package "SocketServer". Cependant cette classe va créer une socket IPv4 par défaut. Si on veut gérer IPv6, on peut créer une sous-classe de ThreadingTCPServer, et redéfinir certaines méthodes de manière à ouvrir une socket IPv6 par défaut, tout en pensant à gérer quelques détails importants :

  • on doit mettre l'option IPV6_V6ONLY à 0 sur la socket IPv6 pour également accepter les clients IPv4
  • si IPv6 n'est pas disponible sur le système, on doit finalement ouvrir une socket IPv4

Ce travail n'est pas trivial mais, une fois réalisé, gérer IPv6 revient juste à utiliser notre nouvelle classe en lieu et place de ThreadingTCPServer.

Perl

L'API bas-niveau IPv6 en perl (librairie Socket6) est également très proche de l'API C. On peut remarquer les inconvénients suivant :

  • Comme en python, le symbole IPV6_V6ONLY n'est pas défini par le langage. On peut cependant définir manuellement une telle constante après avoir récupéré sa valeur dans les fichiers "headers" du langage C.
  • La librairie Socket6 n'est pas incluse dans la distribution standard (on peut la télécharger à l'adresse http://search.cpan.org/dist/Socket6/)
  • L'appel getnameinfo() comportait apparemment encore assez récemment (visible en perl 5.8.0, corrigé en 5.8.5) un bug qui empêchait de l'utiliser pour obtenir la représentation textuelle d'une adresse IPv6.

Perl permet également de programmer de manière plus "haut-niveau". La librairie la plus remarquable concernant la programmation IPv6 est IO::Socket::INET6 (http://search.cpan.org/~shlomif/IO-Socket-INET6/). Cette librairie fournit des fonctionnalités très intéressantes, comme dans l'exemple suivant qui permet, en un seul appel, de créer une socket et d'essayer toutes les familles d'adresses pour se connecter à un serveur :

$sock = IO::Socket::INET6->new(PeerAddr   => 'www.perl.org',
                               PeerPort   => 'http(80)',
                               Multihomed => 1,
                               Proto      => 'tcp');

Attention cependant, si on en croit le fichier README de cette librairie, toutes les fonctionnalités IPv4 ou indépendantes de la famille d'adresse risquent d'être supprimées prochainement. Voici ce qu'indique ce fichier :

WARNING: You should use this module mainly to program IPv6 
domain. Most pobably future releases will not support AF_INET | AF_UNSPEC 
options, as the module seems to fail on some given corner cases. If you require 
IPv4, you are encouraged to use IO::Socket::INET from the application 
level. Be warned.

Java

Le support IPv6 fourni par le langage Java étant assez haut niveau, il a l'avantage de permettre un code moins verbeux et plus concis. Par exemple, le fait de créer un objet ServerSocket() peut être équivalent aux appels successifs des primitives getaddrinfo(), socket(), bind() et listen() dans les autres langages.

Il comporte néanmoins un défaut important :

  • Le paramètre /proc/sys/net/ipv6/bindv6only (sous Linux) doit être à 0 sur le système où est exécuté le programme. Sinon :
    • Un serveur n'acceptera pas les clients IPv4 (car Java n'ouvre qu'une socket, sous Linux, et l'API ne fournit aucun moyen pour mettre IPV6_V6ONLY à 0)
    • Un client ne pourra pas se connecter sur un serveur IPv4 (erreur “Network is unreachable”, voir http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6342561). Le paramètre /proc/sys/net/ipv6/bindv6only étant la valeur par défaut de l'option de socket IPV6_V6ONLY, on concoît bien qu'il puisse affecter le comportement du serveur, par contre le fait que le client soit également affecté est vraiment étonnant.

Test de la compatibilité IPv6 des applications

Dans cette partie seront présentées les méthodes permettant d'évaluer la compatibilité IPv6 des applications.

Observation des sockets ouvertes sur le système

Le test le plus simple, pour évaluer la compatibilité d'un programme utilisant les sockets réseau, est de vérifier quels sont les sockets qu'il ouvre. La commande netstat est disponible pour cela sur les systèmes du type POSIX. Pour un serveur, il faut utiliser l'option -l pour lister les sockets serveur.

$ netstat –lnpt | grep 2000
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address  State  PID/Program name
tcp        0      0 0.0.0.0:2000  0.0.0.0:*        LISTEN 32343/server_two_so
tcp        0      0 :::2000       :::*             LISTEN 32343/server_two_so
$

Pour un client, il ne faut pas utiliser l'option -l, de façon à lister cette fois-ci les sockets connectées.

$ netstat –npt | grep 2001
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address         Foreign Address       State       PID/Program name
tcp        0      0 2001:66:32:7::2:2001  2001:66:32:7::3:54104 ESTABLISHED 8046/server_one_so
tcp        0      0 2001:66:32:7::3:54104 2001:66:32:7::2:2001  ESTABLISHED 8047/client
$  

Une fois que l'on a vérifié que le client se connecte correctement via IPv6, on devra désactiver l'IPv6 sur la machine du client, et vérifier par la même commande que le client est également capable d'utiliser IPv4 pour se connecter. Note : si le client reste connecté un trop court instant au serveur, cette opération peut être difficile voire impossible à faire. Dans ce cas on devra utiliser une des méthodes qui suivent.

Tentatives de connexion

L'observation des sockets d'écoute décrite dans le paragraphe précédent est souvent incomplète. En effet, sous Linux par exemple, pour une socket d'écoute IPv6 donnée, on n'a pas d'indication sur la valeur de l'option IPV6_V6ONLY, et on ne sait donc pas à priori si les clients IPv4 peuvent s'y connecter ou non.

Il convient donc de tester manuellement une connexion avec un client IPv4. L'outil telnet peut faire l'affaire :

$ telnet 127.0.0.1 3333
Trying 127.0.0.1...
telnet: Unable to connect to remote host: Connection refused
$ telnet 127.0.0.1 3334
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
^]

telnet> quit
Connection closed.
$

On voit dans cet exemple que le serveur écoutant sur le port 3334 accepte les clients IPv4 tandis que celui écoutant sur le port 3333 ne les accepte pas.

On peut également vérifier que ces serveurs acceptent les clients IPv6 :

$ telnet ::1 3333
Trying ::1...
Connected to ::1.
Escape character is '^]'.
^]

telnet> quit
Connection closed.
$
$ telnet ::1 3334
Trying ::1...
Connected to ::1.
Escape character is '^]'.
^]

telnet> quit
Connection closed.
$

L'alternative au client telnet est l'outil nc, ainsi que sa version IPv6 nc6, qui fournissent plus d'options et en particulier la possibilité de fonctionner en mode serveur, ce qui permet de tester les clients.

Utilisation des logs de firewall

Pour vérifier que la connexion entre un client et un serveur se fait en utilisant IPv6, ou bien IPv4, on peut utiliser les logs des firewalls. Voici un exemple de configuration de ip6tables qui active des logs vers le fichier /var/log/messages, pour chaque paquet traversant le firewall, préfixés par [IPv6 in] ou [IPv6 out] suivant les cas.

# ip6tables -F
# ip6tables -X
# ip6tables -N LOG_ACCEPT_IN
# ip6tables -A LOG_ACCEPT_IN -j LOG --log-level 6 --log-prefix "[ipv6 in]" 
# ip6tables -A LOG_ACCEPT_IN -j ACCEPT
# ip6tables -N LOG_ACCEPT_OUT
# ip6tables -A LOG_ACCEPT_OUT -j LOG --log-level 6 --log-prefix "[ipv6 out]"
# ip6tables -A LOG_ACCEPT_OUT -j ACCEPT
# ip6tables -A INPUT -j LOG_ACCEPT_IN
# ip6tables -A OUTPUT -j LOG_ACCEPT_OUT
# ip6tables -P INPUT DROP
# ip6tables -P OUTPUT DROP
# ip6tables -P FORWARD DROP

Notes importantes :

  • Pour réellement utiliser la fonctionnalité standard du firewall, cette configuration est à adapter, car dans l'état elle laisse passer tous les paquets.
  • En cas de configuration permanente, il est recommandé d'utiliser un système de rotation de logs paramétré à une fréquence adaptée (toutes les heures par exemple) car ce genre de configuration va rapidement faire grossir le fichier /var/log/messages.
  • On peut faire une configuration similaire avec iptables pour le trafic IPv4.

La commande suivante illustrent la lecture des logs ainsi générés :

# grep "ipv6 out" /var/log/messages | tail -n 3
Sep 30 14:27:09 quarks kernel: [ipv6 out]IN= OUT=eth0 SRC=2001:0660:3302:7006:0000:0000:0000:0008
DST=2001:0660:3302:7000:021d:09ff:fede:dd9b LEN=120 TC=0 HOPLIMIT=64 FLOWLBL=0 PROTO=TCP SPT=22
DPT=45074 WINDOW=3012 RES=0x00 ACK PSH URGP=0 
Sep 30 14:27:09 quarks kernel: [ipv6 out]IN= OUT=eth0 SRC=2001:0660:3302:7006:0000:0000:0000:0008
DST=2001:0660:3302:7000:021d:09ff:fede:dd9b LEN=120 TC=0 HOPLIMIT=64 FLOWLBL=0 PROTO=TCP SPT=22
DPT=45074 WINDOW=3012 RES=0x00 ACK PSH URGP=0 
Sep 30 14:27:09 quarks kernel: [ipv6 out]IN= OUT=eth0 SRC=2001:0660:3302:7006:0000:0000:0000:0008
DST=2001:0660:3302:7000:021d:09ff:fede:dd9b LEN=104 TC=0 HOPLIMIT=64 FLOWLBL=0 PROTO=TCP SPT=22
DPT=45074 WINDOW=3012 RES=0x00 ACK PSH URGP=0
#

Etude des appels réseau effectués par un programme

Afin d'estimer la compatibilité IPv6 des applications, on peut rechercher ou détecter les appels de fonctions non-compatibles IPv6 effectués par un programme (par exemple gethostbyname()), et les structures ou chaines de caractères spécifiques à IPv4 qu'il utilise (par exemple "127.0.0.1").

Recherche à partir du code source

A l'instar d'EGEE, certains projets on développé des outils très simples recherchant ces éléments non-compatibles IPv6. L'alternative est d'utiliser des outils de recherche plus généraux, tels que ceux fournit par les éditeurs de fichiers, la commande grep, etc.

Détection dynamique

Certains outils permettent de détecter dynamiquement les appels de fonction ou appels systèmes réseau effectués par un programme pendant son exécution. Cela fournit un moyen pratique d'évaluer la compatibilité IPv6 d'un programme donné. L'un des avantages est qu'on n'a pas besoin du code source du programme à tester.

Il convient de noter que certaines parties du code du programme testé ne sont sans doute pas exécutées (par exemple les parties qui dépendent d'options en ligne de commande, ou d'une configuration du système). Ces parties non exécutées étant potentiellement non-compatibles IPv6, ce genre d'outils ne peut pas assurer un diagnostique exhaustif de la compatibilité du programme : la vision de ces programmes se limite à un environnement d'exécution donné. Cependant, dans la plupart des cas, la couverture des appels réseau du programme est totale ou quasi-totale, et l'indication donnée par ces outils est donc très utile.

IPv6 CARE

IPv6 CARE est un outil compatible GNU/Linux, développé au cours du projet EGEE, et spécialisé dans le diagnostique de compatibilité IPv6. Il détecte les appels de fonction réseau (appels à la librairie libc) effectués par un programme en cours de fonctionnement, et permet de générer un diagnostique en conséquence.

# ipv6_care check -v ping www.google.fr
IPV6 CARE detected: socket() with [ domain=AF_INET type=unknown protocol=icmp ]
IPV6 CARE detected: inet_aton() with [ cp=www.google.fr ]
IPV6 CARE detected: inet_pton() with [ src=127.0.0.1 ]
IPV6 CARE detected: inet_pton() with [ src=::1 ]
IPV6 CARE detected: inet_pton() with [ src=194.57.137.61 ]
IPV6 CARE detected: inet_pton() with [ src=194.57.137.60 ]
IPV6 CARE detected: inet_pton() with [ src=2001:660:3302:7000::1111 ]
IPV6 CARE detected: socket() with [ domain=AF_INET type=SOCK_DGRAM protocol=ip ]
IPV6 CARE detected: connect() with [ socket=4 address.ip=192.44.77.1 address.port=53 ]
IPV6 CARE detected: socket() with [ domain=AF_INET type=SOCK_DGRAM protocol=ip ]
IPV6 CARE detected: connect() with [ socket=4 address.ip=74.125.43.106 address.port=1025 ]
IPV6 CARE detected: close() with [ fd=4 ]
IPV6 CARE detected: inet_ntoa() with [ in=74.125.43.106 ]
PING www.l.google.com (74.125.43.106) 56(84) bytes of data.
IPV6 CARE detected: gethostbyaddr() with [ addr.ip=74.125.43.106 ]
IPV6 CARE detected: inet_ntoa() with [ in=74.125.43.106 ]
64 bytes from bw-in-f106.google.com (74.125.43.106): icmp_seq=1 ttl=50 time=34.6 ms
IPV6 CARE detected: gethostbyaddr() with [ addr.ip=74.125.43.106 ]
IPV6 CARE detected: inet_ntoa() with [ in=74.125.43.106 ]
64 bytes from bw-in-f106.google.com (74.125.43.106): icmp_seq=2 ttl=50 time=36.7 ms
IPV6 CARE detected: gethostbyaddr() with [ addr.ip=74.125.43.106 ]
IPV6 CARE detected: inet_ntoa() with [ in=74.125.43.106 ]
64 bytes from bw-in-f106.google.com (74.125.43.106): icmp_seq=3 ttl=50 time=35.8 ms
^C
--- www.l.google.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2001ms
rtt min/avg/max/mdev = 34.699/35.763/36.775/0.848 ms
-------------------------------------------------------------------------------
IPv6 diagnosis for 'ping www.google.fr' was generated in: /tmp/ipv6_diagnosis/ping/by_pid/pid_8729
------------------------------------------------------------------------------- 
#

Le répertoire indiqué (ici /tmp/ipv6_diagnosis/ping/by_pid/pid_8729) contient les informations de diagnostique générées par l'outil. Celles-ci comprennent une trace chronologique des appels réseau effectués, et, pour chaque problème de compatibilité IPv6, sa localisation dans le code, une explication sur ce problème et une indication sur la correction à effectuer.

Le lecteur est invité à consulter la documentation fournie sur la page web de l'outil pour plus d'informations.

strace

L'outil strace est un outil présentant l'avantage d'être très commun dans les systèmes du type UNIX. Il permet de détecter et lister les appels système effectués par un programme donné.

On peut l'utiliser pour détecter plus spécifiquement les appels réseau, comme par exemple dans l'exemple suivant.

# strace -e trace=network ping www.google.fr
socket(PF_INET, SOCK_RAW, IPPROTO_ICMP) = 3
socket(PF_FILE, 0x80801 /* SOCK_??? */, 0) = 4
connect(4, {sa_family=AF_FILE, path="/var/run/nscd/socket"...}, 110) = -1 ENOENT (No such file or directory)
socket(PF_FILE, 0x80801 /* SOCK_??? */, 0) = 4
connect(4, {sa_family=AF_FILE, path="/var/run/nscd/socket"...}, 110) = -1 ENOENT (No such file or directory)
socket(PF_INET, SOCK_DGRAM, IPPROTO_IP) = 4
connect(4, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("192.44.77.1")}, 28) = 0
send(4, "h\317\1\0\0\1\0\0\0\0\0\0\3www\6google\2fr\0\0\1\0\1"..., 31, MSG_NOSIGNAL) = 31
...
#

L'étude de cette trace donne des indications sur le fonctionnement du programme et d'assister le diagnostique. Il convient de noter que l'interprétation de cette trace est beaucoup plus difficile à interpréter que pour IPv6 CARE ou ltrace (voir paragraphe suivant) car strace détecte en fait les appels système et non les appels de fonction ; par exemple pour une résolution de nom, plutôt que de ne repérer qu'un seul appel de fonction explicite, on va observer la création d'une socket vers le serveur de noms, l'appel connect(), et l'échange de données.

Le lecteur est invité à consulter la page de manuel pour les usages plus avancés. L'outil fournit par exemple des possibilités de filtrage plus fin, la possibilité de "s'attacher" à un processus en cours de fonctionnement, etc.

ltrace

L'outil ltrace est un outil similaire à strace mais détectant les appels de fonctions (comme IPv6 CARE), et donc générant une trace plus haut niveau (en comparaison à strace) et plus facile à interpréter. Cet outil est cependant moins commun que strace.

Comme pour strace, on peut l'utiliser pour détecter plus spécifiquement les appels de fonction réseau, et étudier cette trace pour évaluer la compatibilité IPv6 d'un programme. Voici un exemple.

# ltrace -e gethostbyaddr,gethostbyname,inet_ntoa,inet_ntop,inet_aton,inet_pton ping www.google.fr 
inet_aton("www.google.fr", 0x08060444)                                                = 0
inet_ntoa(0x638155d1)                                                                 = "209.85.129.99"
PING www.l.google.com (209.85.129.99) 56(84) bytes of data.
gethostbyaddr("\321U\201c@", 4, 2)                                                    = 0xb7fffaa0
inet_ntoa(0x638155d1)                                                                 = "209.85.129.99"
64 bytes from fk-in-f99.google.com (209.85.129.99): icmp_seq=1 ttl=242 time=13.4 ms
gethostbyaddr("\321U\201c@", 4, 2)                                                    = 0xb7fffaa0
inet_ntoa(0x638155d1)                                                                 = "209.85.129.99"
64 bytes from fk-in-f99.google.com (209.85.129.99): icmp_seq=2 ttl=242 time=12.4 ms
^C--- SIGINT (Interrupt) ---

--- www.l.google.com ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 12.433/12.925/13.418/0.505 ms
+++ exited (status 0) +++
#

Méthode de gestion d'IPv6 automatique : IPv6 CARE

Outre sa fonction pour diagnostiquer la compatibilité IPv6 des applications (ipv6_care check <ligne de commande>, voir plus haut), l'outil IPv6 CARE propose une méthode pour corriger le comportement des programmes à la volée, de manière à les rendre compatible IPv6.

L'utilisation est très simple, il suffit de préfixer les commandes par ipv6_care patch. Par exemple, pour rendre le serveur mysql compatible IPv6, on lance le service de la manière suivante :

$ ipv6_care patch /etc/init.d/mysql start

Une commande netstat -lnpt nous informera alors que le processus a ouvert une socket IPv4 et une socket IPv6, au lieu de la seule socket IPv4.

Un test plus complet, incluant l'interaction avec le client mysql (lui aussi patché) est disponible en vidéo à l'adresse https://twiki.cern.ch/twiki/bin/view/EGEE/IPv6CARE.

Cette méthode fonctionne par détection et modification des appels de fonction réseau effectués par les programmes. Elle est apparue avec la version 3.0 de l'outil, qui ne gérait que les programmes TCP, et doit être étendue à un ensemble plus large dans les versions qui suivent.

A l'heure actuelle l'outil n'a été testé que sur des systèmes d'exploitation Linux.

Personal tools