Difference between revisions of "Programmation d'applications bis"
From Livre IPv6
(→Etude des appels réseau effectués par un programme) |
(→Test de la compatibilité IPv6 des applications) |
||
Line 1,699: | Line 1,699: | ||
==Test de la compatibilité IPv6 des applications== | ==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=== | ===Observation des sockets ouvertes sur le système=== | ||
===Tentatives de connexion=== | ===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 <tt>IPV6_V6ONLY</tt>, et on ne sait donc pas à priori si les clients IPv4 peuvent s'y connecter ou non. | ||
+ | |||
+ | Il convient donc de tester manuellement une connection avec un client IPv4. L'outil <tt>telnet</tt> 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 <tt>telnet</tt> est l'outil <tt>nc</tt>, ainsi que sa version IPv6 <tt>nc6</tt>, qui fournissent plus d'options et en particulier la possibilité de fonctionner en mode serveur, ce qui permet de tester les clients. | ||
===Etude des appels réseau effectués par un programme=== | ===Etude des appels réseau effectués par un programme=== |
Revision as of 09:09, 20 July 2009
Contents
- 1 Résumé
- 2 Gestion d'IPv6 avec l'API socket classique
- 2.1 Généralités
- 2.2 Liste des changements
- 2.3 Changements impliquant un travail de migration important
- 2.4 Ce qui ne change pas
- 2.5 Programmation sur un système double-pile
- 2.6 Programmation indépendante de la famille d'adresses
- 2.7 Exemples
- 3 L'API avancée
- 4 Les langages de programmation et IPv6
- 5 Test de la compatibilité IPv6 des applications
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.
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 le flag AI_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.
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 traffic IPv4 (en orange sur la figure ci-contre) et IPv6 (en bleu). Le serveur doit ensuite écouter simultanément sur ces deux sockets (utilisation d'un select()). | Figure : Serveur utilisant une socket PF_INET et une socket PF_INET6 |
La deuxième solution est d'ouvrir une seule socket PF_INET6, qui acceptera à la fois les clients IPv4 et IPv6. Dans ce cas les adresses IPv4 sont traduites en adresses "IPv4 mappées en IPv6" (traffic jaune sur la figure ci-contre) de façon à ce que le traffic IPv4 puisse atteindre la socket PF_INET6. Cette traduction est faite par le système d'exploitation, et activée ou désactivée suivant la valeur de l'option de socket IPV6_V6ONLY. | Figure : Serveur utilisant une seule socket PF_INET6 |
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:YYYY où XXXXYYYY 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.
Si cette option n'est pas définie, la valeur par défaut dépend du système d'exploitation sur lequel le serveur est exécuté. Par exemple, sur Linux/*BSD la valeur par défaut est donnée (et modifiable) par sysctl net.ipv6.bindv6only. Ceci donne 0 par défaut sous Linux, et 1 sur *BSD...
En conséquence, un programme qui se veut portable doit explicitement donner une valeur à cette option.
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.
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 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 Canonical name: bernays.ipv6.logique.jussieu.fr Adresses: 2001:660:101:101:200:f8ff:fe31:17ec 3ffe:304:101:1:200:f8ff:fe31:17ec $ haah 134.157.19.71 Canonical name: bernays.logique.jussieu.fr Adresses: 134.157.19.71 $
Le programme réalisant la commande haah ne présente aucune difficulté. C'est une simple application des primitives précédemment décrites.
1| #include <stdio.h> </tt> 2| 3| #include <string.h> 4| #include <errno.h> 5| #include <sys/types.h> 6| #include <sys/socket.h> 7| #include <netinet/in.h> 8| #include <netdb.h> 9| #include <arpa/inet.h> 10| 11| 12| int main(int argc, char **argv) 13| { 14| int ret; 15| struct addrinfo *res, *ptr; 16| struct addrinfo hints = { 17| AI_CANONNAME, 18| PF_UNSPEC, 19| SOCK_STREAM, 20| 0, 21| 0, 22| NULL, 23| NULL, 24| NULL 25| }; 26| 27| if (argc != 2) { 28| fprintf(stderr, "%s: usage: %s host | addr.\n", *argv, *argv); 29| exit(1); 30| } 31| ret = getaddrinfo(argv[1], NULL, &hints, &res); 32| if (ret) { 33| fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret)); 34| exit(1); 35| } 37| for (ptr = res; ptr; ptr = ptr->ai_next) { 38| if (ptr->ai_canonname) 39| fprintf(stdout,"Canonical name:\n%s\nAdresses:\n", ptr->ai_canonname); 40| switch (ptr->ai_family) { 41| case AF_INET: 42| { 43| char dst[INET_ADDRSTRLEN]; 44| struct in_addr *src = &((struct sockaddr_in *) ptr->ai_addr)->sin_addr; 45| 46| if(!inet_ntop(AF_INET, (const void *) src, dst, sizeof(dst))) { 47| fprintf(stderr, "inet_ntop: %s\n", strerror(errno)); 48| break; 49| } 50| fprintf(stdout, "%s\n", dst); 51| break; 52| } 53| case AF_INET6: 54| { 55| char dst[INET6_ADDRSTRLEN]; 56| struct in6_addr *src=&((struct sockaddr_in6 *)ptr->ai_addr)->sin6_addr; 57| 58| if (!inet_ntop(AF_INET6, (const void *) src, dst, sizeof(dst))) { 59| fprintf(stderr, "inet_ntop: %s\n", strerror(errno)); 60| break; 61| } 62| fprintf(stdout, "%s\n", dst); 63| break; 64| } 65| default: 66| fprintf(stderr, "getaddrinfo: %s\n", strerror(EAFNOSUPPORT)); 67| } 68| } 69| freeaddrinfo(res); 70| exit(0); 71| }
Client/Serveur TCP (langage C)
Vue d'ensemble
Le client/serveur choisi est particulièrement simple quant à sa fonction de service, de façon à privilégier l'aspect réseau dans la présentation. Il calcule le nombre d'utilisateurs connectés sur une machine donnée. Plus précisément :
$ nbus bernays 3 user(s) logged on bernays $
Il se compose de cinq fichiers sources :
- nbus.h le fichier d'en-tête commun aux fichiers sources du client et du serveur
- nbus.c le fichier source principal du client
- open_conn.c le fichier source qui gère les connexions du côté client
- nbusd.c le fichier source du serveur propre au service
- serv_daemon.c le fichier source qui gère les connexions du côté serveur
Le client, prend en argument un nom de machine (ou une adresse numérique), le convertit en une adresse IPv4 ou IPv6 et ainsi envoie ses requêtes à un serveur.
Le fichier source du serveur, selon les options de compilation, génère un code IPv6 ou un code IPv4. Dans le premier cas, le serveur est à même de satisfaire les requêtes d'un client IPv6 mais aussi d'un client IPv4. Dans le second cas, seuls les clients IPv4 pourront être pris en compte.
Il faut noter qu'il existe deux modes de fonctionnement pour une machine à double pile IPv4 et IPv6, la figure Le client/serveur nbus résume la situation. Soit les espaces d'adresses sont disjoints, et dans ce cas il faut deux serveurs, un qui écoute les requêtes IPv4 (nbus4d) et un qui écoute les requêtes IPv6 (nbus6d) (ou un seul serveur, nbusd, avec deux sockets IPv4 et IPv6 séparées). Soit l'espace d'adresse IPv4 est inclus dans l'espace IPv6 et dans ce cas il suffit d'un serveur IPv6 (nbus6d) qui recevra les requêtes venant en IPv4 comme une requête IPv6 venant d'une adresse "IPv4 mappée".
Suivant les systèmes le mode de fonctionnement est prédéfini, configurable globalement dans le noyeau, ou configurable séparément pour chaque socket IPv6. Ainsi en FreeBSD le mode par défaut est "espace partagé" (choix modifiable par "sysctl -w net.inet6.ip6.v6only=1") et modifiable pour chaque socket, en précisant tcp4, tcp6 ou tcp46 dans le fichier de configuration d'inetd, ou en utilisant "setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY,&val)" dans le code du serveur.
Deux mêmes programmes serveur ne peuvent s'exécuter en même temps sur une machine : chaque serveur réserve le port nbus (par un bind) et par suite si un serveur est lancé, tout autre serveur échouera avec une erreur "port en service" (EADDRINUSE). Cela peut être aussi vrai entre les deux types de serveurs, nbus4d et nbus6d sur une machine "double pile avec espace partagé", car les espaces de service TCP et UDP sont communs à IPv4 et IPv6 et un port en service l'est aussi bien en IPv4 qu'en IPv6 ; simplement si le port correspond à une socket PF_INET, une requête de connexion IPv6 sera rejetée avec une erreur "port inaccessible". Dans la réalité le comportement est plus complexe. Même en mode "double pile avec espace partagé", on peut avoir deux sockets, l'une PF_INET et l'autre PF_INET6, comme le montre le programme nbusd.
Le code des différents serveurs est très semblable, le choix est fait en donnant une option à la compilation : nbusd par défaut, nbus4d si on ajoute l'option -DIPV4,et nbus6d si on ajoute l'option -DIPV6.
Le fichier d'en-tête nbus.h ne contient que le nom du service correspondant.
#define SERVICE "nbus"
Ainsi doit-on trouver, par exemple, dans le fichier /etc/services des machines concernées, la ligne suivante :
nbus 20000/tcp
Le programme client nbus établit tout d'abord une connexion TCP avec la machine cible (donnée en argument à nbus) via la fonction open_conn (cette fonction sera décrite ci-après). Il lit ensuite un entier court, résultat du calcul du nombre d'utilisateurs connectés sur la machine cible, puis l'imprime.
On notera la présence de la macro ntohs rendue nécessaire du fait des représentations différentes des entiers selon les machines.
1| #include <stdio.h> 2| #include <unistd.h> 3| 4| #include "nbus.h" 5| 6| extern open_conn(char *, char *); 7| 8| int main(int argc, char **argv) 9| { 10| int sock; 11| short nu; 12| 13| if (argc != 2) { 14| fprintf(stderr, "Usage: %s host\n", argv[0]); 15| exit(1); 16| } 17| if ((sock = open_conn(argv[1], SERVICE)) < 0) 18| exit(1); 19| read(sock, (char *) &nu, sizeof(nu)); 20| nu = ntohs(nu); 21| if (nu == -1) { 22| fprintf(stderr, "Can't read \"utmp\" on %s\n", argv[1]); 23| exit(1); 24| } 25| if (nu) { 26| fprintf(stdout, "%d user(s) logged on %s\n", nu, argv[1]); 27| exit(0); 28| } 29| fprintf(stdout, "Nobody on %s\n", argv[1]); 30| exit(0); 31| }
Le serveur nbusd, quant à lui, lance en tâche de fond un processus "démon" qui exécutera la fonction de service nbus (lignes 28 à 60) à chaque requête d'un client. Ce processus démon est réalisé par la fonction serv_daemon.
1| #include <stdio.h> 2| #include <unistd.h> 3| #include <fcntl.h> 4| #include <utmp.h> 5| #include <sys/socket.h> 6| 7| #include "nbus.h" 8| 9| 10| #if defined(IPV6) 11| #define FAMILY PF_INET6 12| #elif defined(IPV4) 13| #define FAMILY PF_INET 14| #else 15| #define FAMILY PF_UNSPEC 16| #endif 17| 18| extern serv_daemon(int, char *, void (*)(int), char *); 19| 20| void nbus(int); 21| 22| int main(void) 23| { 24| serv_daemon(FAMILY, SERVICE, nbus, NULL); 25| exit(0); 26| } 27| 28| void nbus(int sock) 29| { 30| short nu = -1; 31| #ifdef USER_PROCESS /* Solaris/Linux, use getutent */ 32| struct utmp *up; 33| 34| up = getutent(); 35| if (up != NULL) { 36| for (nu = 0; up != NULL; up = getutent()) 37| if (up->ut_type == USER_PROCESS) 38| nu++; 39| } 40| endutent(); 41| #else /* *BSD read directly utmp file */ 42| #ifndef UTMP 43| #define UTMP "/var/run/utmp" /* for FreeBSD/NetBSD */ 44| #endif 45| 46| struct utmp ut; 47| int fd; 48| 49| if ((fd = open(UTMP, O_RDONLY)) >= 0) { 50| nu = 0; 51| while (read(fd, (char *) &ut, sizeof(ut)) == sizeof(ut)) 52| if (ut.ut_name[0]) 53| nu++; 54| } 55| close(fd); 56| #endif 57| nu = htons(nu); 58| write(sock, (char *) &nu, sizeof(nu)); 59| return; 60| }
L'établissement d'une connexion TCP, côté client
Comme on l'a vu plus haut dans le code du client nbus.c, l'établissement de la connexion TCP se fait au moyen de la fonction open_conn. Cette fonction prend en premier argument le nom de la machine avec laquelle on va établir la connexion TCP et en deuxième argument le nom du service tel qu'il apparaît dans le fichier /etc/services. La valeur retournée par open_conn est soit -1 en cas d'erreur, soit le descripteur associé à la socket réalisant la connexion. La figure Algorithme du client visualise l'algorithme employé.
La première étape sera la construction de l'adresse de la socket distante, ceci via la primitive getaddrinfo. On remarquera que le champ ai_family de la structure hints a été initialisé à la valeur PF_UNSPEC, ce qui signifie que, suivant que l'on donne en argument à la commande nbus un nom de machine (ou une adresse numérique) IPv4 ou IPv6, on travaillera avec une socket soit dans la famille des protocoles PF_INET, soit dans la famille des protocoles PF_INET6. Si on avait fait le choix de forcer la famille des protocoles à la valeur PF_INET6, il aurait fallu initialiser les champs ai_flags et ai_family respectivement aux valeurs AI_V4MAPPED et PF_INET6 (les adresses IPv4 seraient dans ce cas "mappées"). Si, comme dans l'exemple proposé, on ne fait pas ce choix, on notera qu'il n'y a aucune différence entre le code IPv4 et le code IPv6.
Ensuite, après avoir créé une socket, on la connecte au serveur de la machine cible (primitive connect). Là encore, le code est le même pour IPv4 et IPv6. Remarquons que getaddrinfo peut avoir rendu plusieurs valeurs. Un programme plus évolué devrait essayer de se connecter à chaque adresse rendue jusqu'à obtenir une réponse.
1| #include <stdio.h> 2| #include <unistd.h> 3| #include <sys/socket.h> 4| #include <netdb.h> 5| 6| int open_conn(char *host, char *serv) 7| { 8| int sock, ecode; 9| struct addrinfo *res; 10| struct addrinfo hints = { 11| 0, 12| PF_UNSPEC, 13| SOCK_STREAM, 14| 0, 15| 0, 16| NULL, 17| NULL, 18| NULL 19| }; 20| 21| ecode = getaddrinfo(host, serv, &hints, &res); 22| 23| if (ecode) { 24| fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ecode)); 25| exit(1); 26| } 27| 28| if ((sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol)) < 0) { 29| freeaddrinfo(res); 30| perror("socket"); 31| return -1; 32| } 33| 34| if (connect(sock, res->ai_addr, res->ai_addrlen) < 0) { 35| close(sock); 36| freeaddrinfo(res); 37| perror("connect"); 38| return -1; 39| } 40| freeaddrinfo(res); 41| return sock; 42| }
Le serveur
Le serveur proprement dit est réalisé par une unique fonction. C'est la fonction serv_daemon qui a quatre arguments. Le premier est la famille d'adresse gérée, PF_INET ou PF_INET6, ou PF_UNSPEC pour écouter dans les deux familles. Le deuxième est, comme dans le cas de open_conn, le nom du service tel qu'il apparaît dans le fichier /etc/services. Le troisième argument est un pointeur vers la fonction de service dont le type (int(*)(void)) sera commenté ultérieurement. Enfin le dernier argument est le nom passé au démon syslogd lors de l'impression des messages d'erreur (ligne See ). Si le dernier argument est le pointeur nul, ce nom est par défaut le nom du service. Un aperçu du déroulement de la fonction serv_daemon est donné par la figure Algorithme du serveur.
1| #include <stdio.h> 2| #include <stdlib.h> 3| #include <unistd.h> 4| #include <errno.h> 5| #include <sys/socket.h> 6| #include <netdb.h> 7| #include <signal.h> 8| #include <syslog.h> 9| #include <sys/select.h> 10| #include <sys/wait.h> 11| 12| static void reap_child(int); 13| 14| void serv_daemon(int family, char *serv, void (*serv_funct)(int), char *serv_logname) 15| { 16| int sock[2], ecode, n = 0; 17| struct addrinfo *res, *rres, hints; 18| 19| memset(&hints, 0, sizeof hints) ; 20| hints.ai_flags = AI_PASSIVE; 21| hints.ai_socktype = SOCK_STREAM; 22| hints.ai_family = family; 23| ecode = getaddrinfo(NULL, serv, &hints, &rres); 24| if (ecode) { 25| fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ecode)); 26| exit(1); 27| } 28| for (res = rres; res; res = res->ai_next) { 29| if (n == 2) { /* au plus 2 : anyaddr PF_INET et anyaddr PF_INET6 */ 30| fprintf(stderr, "erreur interne: trop d'adresses\n"); 31| exit(1); 32| } 33| sock[n] = socket(res->ai_family, res->ai_socktype, res->ai_protocol); 34| if (sock[n] < 0) { 35| perror("socket"); 36| exit(1); 37| } 38| if (bind(sock[n], res->ai_addr, res->ai_addrlen) < 0) { 39| perror("bind"); 40| exit(1); 41| } 42| listen(sock[n], SOMAXCONN); 43| n++; 44| #ifdef __linux__ 45| /* En Linux, utiliser seulement la premiere 46| * reponse, sinon on a un conflit sur bind */ 47| break; 48| #endif 49| } 50| freeaddrinfo(rres);
La première étape consiste en la préparation de l'adresse de la socket d'écoute du serveur en faisant appel, comme d'habitude, à la primitive getaddrinfo. Comme il s'agit d'une socket d'écoute, le champ ai_flags de la structure hints a été initialisé à la valeur AI_PASSIVE tandis que le champ ai_family de cette même structure a lui été initialisé à la valeur passé en argument à l'appel.
Pour chaque réponse de getaddrinfo (une seule si la famille est définie, deux si la famille est PF_UNSPEC), on crée une socket d'écoute, puis effectue son attachement en faisant appel à la primitive bind. La socket d'écoute créée et attachée à une adresse, le serveur doit signifier au système qu'il est prêt à accepter les demandes de connexions. C'est l'objet de la primitive listen dont le deuxième argument SOMAXCONN (macro-définition que l'on trouve dans le fichier sys/socket.h) est le nombre maximal de connexions pendantes.
Dans les lignes qui suivent (51 à 58), le processus est détaché du terminal de contrôle et est lancé en tâche de fond (processus "démon"), sauf en Solaris, le code étant trop différent, si le fichier source n'est pas compilé avec l'option -DDEBUG (dans le cas contraire, cela peut être bien utile lors de la phase de mise au point).
51| #ifndef DEBUG 52| #ifndef sun /* no daemon function in Solaris */ 53| if (daemon(0, 0) < 0) { 54| perror("daemon"); 55| exit(1); 56| } 57| #endif 58| #endif
Dans la boucle sans fin (lignes See à See ), on attend les connexions. Comme il peut y avoir plusieurs sockets actives, on utilise select (lignes See à See ) pour attendre sur toutes les sockets ouverts. Au retour de select, on teste les sockets qui ont des connexions pendantes (ligne See ), chaque connexion pendante dans la file d'attente associée à la socket d'écoute est extraite par le serveur et le circuit virtuel avec la socket du client est établi via la création d'une nouvelle socket. Ce travail est effectué par la primitive accept dont la valeur de retour est le descripteur d'E/S de la socket nouvellement créée. Ensuite le serveur crée un processus fils (ligne See ) qui exécutera la fonction de service à laquelle on passe en argument la valeur retournée par la primitive accept.
Il faut également veiller à ce que chaque processus fils, lors de sa terminaison (qui se produit à la fin de l'exécution de la fonction de service), ne devienne un processus "zombie", ce qui à terme peut provoquer une saturation de la table des processus. Pour cela il faut, sous UNIX BSD, capter le signal SIGCHLD (ligne See , fonction reap_child). Notons qu'en SYSTEM V, on pourrait simplement ignorer le signal SIGCLD.
59| signal(SIGCHLD, reap_child); 60| if (!serv_logname) 61| serv_logname = serv; 62| openlog(serv_logname, LOG_PID | LOG_CONS, LOG_USER); 63| for (;;) { 64| int a, f, len, fd, m = -1; 65| struct sockaddr_storage from; 66| fd_set fdset; 67| 68| FD_ZERO(&fdset); 69| for (fd = 0; fd < n; fd++) { 70| if (m < sock[fd]) 71| m = sock[fd]; 72| FD_SET(sock[fd], &fdset); 73| } 74| ecode = select(m+1, &fdset, NULL, NULL, NULL); 75| if (ecode < 0) 76| syslog(LOG_ERR, "%s: select: %m", serv); 77| if (ecode <= 0) 78| break; 79| for (fd = 0; fd < n; fd++) { 80| if (FD_ISSET(sock[fd], &fdset)) { 81| len = sizeof from; 82| a = accept(sock[fd], (struct sockaddr *)&from, &len); 83| if (a < 0) { 84| if (errno != EINTR) 85| syslog(LOG_ERR, "%s: accept: %m", serv); 86| continue; 87| } 88| f = fork(); 89| if (f == 0) { 80| /* Par correction, il faudrait fermer dans le fils 90| * tous les descripteurs hérités (i.e. tous sauf a) */ 91| serv_funct(a); 92| exit(0); 93| } 94| close(a); 95| if (f == -1) { 96| syslog(LOG_ERR, "%s: fork: %m", serv); 97| continue; 98| } 99| } 100| } 101| } 102| } 103| 104| static void reap_child(int sig) 105| { 106| int status; 107| 108| while (wait3(&status, WNOHANG, NULL) > 0); 109| }
Une dernière remarque toujours à propos de la similitude des codes IPv6 et IPv4 : la seule différence dans le code de serv_daemon entre IPv4 et IPv6 est la valeur du premier argument, qui définit la famille d'adresses écoutée.
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).
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> :
- 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) :
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);
où hoplimit est un entier qui a été précédemment déclaré dans le corps de la fonction wait_for_echo_reply6 et recv_data a pour texte :
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 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).
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
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
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 connection 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.
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 (par exemple "127.0.0.1") qu'il utilise.
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 recvfrom(4, "h\317\201\200\0\1\0\10\0\7\0\0\3www\6google\2fr\0\0\1\0\1\300\f"..., 1024, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("192.44.77.1")}, [16]) = 287 ... #
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) +++ #