Programmation d'applications bis
From Livre IPv6
Contents
- 1 HISTORIQUE DU TRAVAIL SUR CE CHAPITRE
- 2 Résumé (TEXTE REPRIS DE L'ANCIENNE VERSION)
- 3 L'interface de programmation "socket" IPv6 (TEXTE REPRIS DE L'ANCIENNE VERSION)
- 4 L'interface socket
- 5 L'adresse "wildcard"
- 6 L'adresse de bouclage
- 7 Les primitives de conversion entre noms et adresses
- 8 Les fonctions de conversion numériques d'adresses
HISTORIQUE DU TRAVAIL SUR CE CHAPITRE
- 2009-06-30: Copié-Collé séquentiel de l'ancienne Version sur cette page (Etienne)
Résumé (TEXTE REPRIS DE L'ANCIENNE VERSION)
L'évolution de IPv4 vers IPv6 a été conçue pour minimiser les changements visibles. Un grand nombre de concepts n'ont pas changé : les noms, les "ports", l'envoi et la réception de données,... Un certain nombre de points ont malgré tout dû être modifiés. Le principal est lié à la taille de l'adresse : en IPv4, une adresse a une longueur de 32 bits (et de nombreux programmes confondent les types adresse et entier) alors qu'en IPv6 une adresse a une longueur de 128 bits ; les types liés aux adresses doivent donc être modifiés. En fait l'effet est plus profond : les nouvelles structures sont plus grandes, et certaines réservations de mémoire avec conversion de type implicite (en particulier : un entier pour une adresse, une struct sockaddr pour une struct sockaddr_in, un tampon de 16 octets pour afficher une adresse sous forme numérique) doivent être corrigés sous peine de débordement de mémoire.
L'interface de programmation réseau ("API") la plus connue est l'interface "socket" (dite aussi interface "BSD"). Le but de ce chapitre est de présenter pour cette interface de programmation les modifications introduites pour supporter IPv6, et notamment de donner une brève description des nouvelles primitives d'appel au DNS et de conversion d'adresses.
Ces modifications ont été définies pour être aussi transparentes que possible, et, s'il est en pratique toujours nécessaire de modifier un programme pour le porter de IPv4 à IPv6, un programme conçu avec des règles de typage strict est portable sans grandes modifications.
Ce chapitre illustrera l'interface de programmation "socket" pour IPv6 en présentant plusieurs exemples de programmes. Plus précisément, il détaillera successivement :
- un programme combinant les différentes fonctions de conversion d'adresse ;
- un client/serveur TCP calculant le nombre d'utilisateurs connectés sur une machine cible. En particulier, on aura soin de comparer les codes IPv4 et IPv6 de ce client/serveur, ce qui amènera à constater qu'à ce niveau de programmation, la migration vers IPv6 n'offre aucune difficulté ;
- un "mini ping" qui permettra de se familiariser avec le protocole ICMPv6 qui présente de notables différences avec son prédécesseur le protocole ICMPv4 ;
- un exemple qui génère un trafic multicast, avec abonnement et désabonnement ;
- un programme illustant l'utilisation de l'API socket avancée.
L'interface de programmation "socket" IPv6 (TEXTE REPRIS DE L'ANCIENNE VERSION)
Ce qui a changé
Les changements opérés de façon à intégrer IPv6 concernent les quatre domaines suivants :
- les structures de données d'adresses ;
- l'interface socket ;
- les primitives de conversion entre noms et adresses ;
- les fonctions de conversions d'adresses.
Ces changements ont été minimisés autant que possible de manière à faciliter le portage des applications IPv4 existantes. En outre, et ce point est important, cette nouvelle API doit permettre l'interopérabilité entre machines IPv4 et machines IPv6 grâce au mécanisme de double pile décrit ci-après.
L'API décrite ici est celle utilisée en Solaris, Linux et systèmes *BSD. Elle correspond à celle définie dans le RFC 3493 avec quelques modifications nécessaires pour prendre en compte les dernières évolutions des protocoles sous-jacents. Cette API est explicitement conçue pour fonctionner sur des machines possédant la double pile IPv4 et IPv6 (cf. See Double pile IPv4/IPv6 pour le schéma d'implémentation d'une telle double pile sous UNIX 4.4BSD). Cette API "socket" est celle disponible dans de nombreux environnements de programmation tels que Java, perl, python, ruby, ...
Une API "avancée", décrite dans le RFC 3542 permet de programmer les échanges réseaux de manière très précise. Elle sera également utilisée mais de manière succinte et essentiellement par le biais de l'exemple one_ping6.
Les structures de données d'adresses
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
La structure de données destinée à contenir une adresse IPv6 est définie comme suit (dans netinet/in.h) :
struct in6_addr { uint8_t s6_addr[16]; };
les octets constituant l'adresse étant rangés comme d'habitude dans l'ordre réseau (network byte order).
La structure de données IPv6 struct sockaddr_in6, est équivalente à la structure struct sockaddr_in d'IPv4. Elle est définie comme suit (dans netinet/in.h) pour les systèmes dérivés d'UNIX 4.3BSD :
struct sockaddr_in6 { sa_family_t sin6_family; /* AF_INET6 */ in_port_t sin6_port; /* numéro de port */ uint32_t sin6_flowinfo; /* identificateur de flux */ struct in6_addr sin6_addr; /* adresse IPv6 */ uint32_t sin6_scope_id; /* ensemble d'interfaces correspondant * à la portée de l'adresse */ };
Il faut noter que cette structure a une longueur de 28 octets, et est donc plus grande que le type générique struct sockaddr. Il n'est donc plus possible de réserver une struct sockaddr si la valeur à stocker peut être une struct sockaddr_in6. Afin de faciliter la tâche des implémenteurs, une nouvelle structure de données, struct sockaddr_storage, a été définie. Celle-ci est de taille suffisante afin de pouvoir prendre en compte tous les protocoles supportés et alignée de telle sorte que les conversions de type entre pointeurs vers les structures de données d'adresse des protocoles supportés et pointeurs vers elle-même n'engendrent pas de problèmes d'alignement. Un exemple d'utilisation pourrait être le suivant :
struct sockaddr_storage ss; struct sockaddr_in *sin = (struct sockaddr_in *) &ss; struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) &ss;
Dans la version 4.4 d'UNIX BSD, la longueur du champ sin6_family est passée de 2 octets à 1 octet. L'octet ainsi récupéré contient la taille de la structure sockaddr_in6 et sert à effectuer correctement la conversion de type vers la structure de données générique sockaddr utilisée par bon nombre de primitives de l'interface socket.
La macro-définition SIN6_LEN, présente dans toute implémentation 4.4BSD, permet alors de distinguer les versions. Les autres champs restant inchangés, cette structure est presque identique à celle de la précédente version :
#define SIN6_LEN struct sockaddr_in6 { u_int8_t sin6_len; /* la longueur de cette structure */ sa_family_t sin6_family; /* AF_INET6 */ in_port_t sin6_port; /* numéro de port */ uint32_t sin6_flowinfo; /* identificateur de flux */ struct in6_addr sin6_addr; /* adresse IPv6 */ uint32_t sin6_scope_id; /* ensemble d'interfaces correspondant * à la portée de l'adresse */ };
Si le champ sin6_len existe (ce qui est testable par le fait que le symbole SIN6_LEN est défini), il doit être initialisé par la taille de la structure sockaddr_in6.
On notera la présence de deux nouveaux champs (ils n'ont pas d'équivalents dans la structure sockaddr_in) dans la structure de données sockaddr_in6, les champs sin6_flowinfo et sin6_scope_id. Le premier, en réalité structuré, est décrit dans le RFC 2460 et Identificateur de flux. Le second désigne un ensemble d'interfaces en adéquation avec la portée de l'adresse contenue dans le champ sin6_addr. Par exemple, si l'adresse en question est de type lien local, le champ sin6_scope_id devrait être un index d'interface.
L'interface socket
La création d'une socket se fait comme auparavant en appelant la primitive socket. La distinction entre les protocoles IPv4 et IPv6 se fait sur la valeur du premier argument passé à socket, à savoir la famille d'adresses (ou de protocoles), c'est-à-dire ici PF_INET ou PF_INET6. Par exemple, si on veut créer un socket IPv4/UDP, on écrira :
sock = socket(PF_INET, SOCK_DGRAM, 0);
tandis qu'une création de socket IPv6/UDP se fera ainsi :
sock = socket(PF_INET6, SOCK_DGRAM, 0);
Une erreur de programmation classique consiste à utiliser AF_INET à la place de PF_INET. Cela n'a pas d'effet en général car rares sont les systèmes pour lesquels ces deux constantes diffèrent. Pour éviter en IPv6 des problèmes liés à cette erreur, il est demandé que les deux constantes PF_INET6 et AF_INET6 soient identiques.
Quant aux autres primitives constituant l'interface socket, leur syntaxe reste inchangée. Il faut simplement leur fournir des adresses IPv6, en l'occurrence des pointeurs vers des structures de type struct sockaddr_in6 au préalable convertis en des pointeurs vers des structures génériques de type struct sockaddr.
Donnons pour mémoire une liste des primitives les plus importantes :
bind() connect() sendmsg() sendto() accept() recvfrom() recvmsg() getsockname() getpeername()
L'adresse "wildcard"
Lors du nommage d'une socket via la primitive bind, il arrive fréquemment qu'une application (par exemple un serveur TCP) laisse au système la détermination de l'adresse source pour elle. En IPv4, pour ce faire, elle passe à bind une structure sockaddr_in avec le champ sin_addr.s_addr ayant pour valeur la constante INADDR_ANY, constante définie dans le fichier netinet/in.h.
En IPv6, il y a deux manières de faire cela, à cause des règles du langage C sur les initialisations et affectations de structures. La première est d'initialiser une structure de type struct in6_addr par la constante IN6ADDR_ANY_INIT :
struct in6_addr any_addr = IN6ADDR_ANY_INIT;
Attention, ceci ne peut se faire qu'au moment de la déclaration. Par exemple le code qui suit est incorrect (en C il est interdit d'affecter une constante complexe à une structure) :
struct sockaddr_in6 sin6; sin6.sin6_addr = IN6ADDR_ANY_INIT; /* erreur de syntaxe !! */
La seconde manière utilise une variable globale :
extern const struct in6_addr in6addr_any; struct sockaddr_in6 sin6; sin6.sin6_addr = in6addr_any;
Cette méthode n'est pas possible dans une déclaration de variable globale ou statique.
La constante IN6ADDR_ANY_INIT et la variable in6addr_any sont toutes deux définies dans le fichier netinet/in.h.
L'adresse de bouclage
En IPv4, c'est la constante INADDR_LOOPBACK. En IPv6, de manière tout à fait similaire à l'adresse "wildcard", il y a deux façons d'affecter cette adresse. Ceci peut se faire au moment de la déclaration avec la constante IN6ADDR_LOOPBACK_INIT :
struct in6_addr loopback_addr = IN6ADDR_LOOPBACK_INIT;
ou via la variable globale in6addr_loopback :
extern const struct in6_addr in6addr_loopback; struct sockaddr_in6 sin6;
sin6.sin6_addr = in6addr_loopback;
Cette constante et cette variable sont définies dans le fichier netinet/in.h.
Les primitives de conversion entre noms et adresses
Les primitives gethostbyname, gethostbyaddr, getservbyname et getservbyport ont été remplacées par les deux primitives indépendantes de la famille d'adresses et normalisées par la RFC 3493 getaddrinfo et getnameinfo :
#include <sys/socket.h> #include <netdb.h> int getaddrinfo(const char *nodename, const char *servname, const struct addrinfo *hints, struct addrinfo **res); void freeaddrinfo(struct addrinfo *res); const char *gai_strerror(int errcode);
Le type struct addrinfo est défini comme suit :
struct addrinfo { int ai_flags; /* AI_PASSIVE, AI_CANONNAME, ... */ int ai_family; /* PF_xxx */ int ai_socktype; /* SOCK_xxx */ int ai_protocol; /* 0 ou IPPROTO_xxx pour IPv4 et IPv6 */ size_t ai_addrlen; /* la taille de l'adresse binaire ai_addr */ char *ai_canonname; /* le nom complétement qualifié */ struct sockaddr *ai_addr; /* l'adresse binaire */ struct addrinfo *ai_next; /* la structure suivante dans la liste chaînée */ };
getaddrinfo prend en entrée le nom d'une machine (nodename) et le nom d'un service (servname). S'il n'y a pas d'erreur, getaddrinfo rend 0 et res pointe sur une liste dynamiquement allouée de struct addrinfo. Chaque élément de cette liste contient la description et l'adresse d'une struct sockaddr initialisée pour fournir l'accès au service servname sur nodename. Les champs ai_family, ai_socktype et ai_protocol ont la valeur utilisable dans l'appel système socket.
Lorsque la liste de résultat n'est plus nécessaire, la mémoire allouée peut être libérée par la primitive freeaddrinfo. En cas d'erreur, getaddrinfo rend un code d'erreur non nul qui peut être imprimé par la fonction gai_strerror.
getaddrinfo peut donner des réponses de la famille d'adresses IPv4 ou IPv6, et des réponses pour les protocoles connectés ou non (ai_socktype peut valoir SOCK_DGRAM ou SOCK_STREAM). L'argument hints permet de choisir les réponses souhaitées. Un argument égal à NULL signifie que la liste des réponses doit contenir toutes les adresses et tous les protocoles. Sinon hints doit pointer sur une structure dont les champs ai_family, ai_socktype et ai_protocol définissent les types de résultat attendus. Une valeur de PF_UNSPEC du champ ai_family signifie que toutes les familles d'adresse (IPv4 et IPv6) sont admises, un 0 dans les champs ai_socktype (resp. ai_protocol) signifie que tous les types de socket (resp. protocole) sont admis. Le champ ai_flags permet de préciser des options supplémentaires.
L'argument servname peut être le nom d'un service ou un nombre décimal. De même, l'argument nodename peut être un nom (au format DNS habituel) ou une adresse sous forme numérique IPv4 ou IPv6 (si ai_flags contient le bit AI_NUMERICHOST, nodename doit être sous forme numérique et aucun appel au serveur de nom n'est fait). De plus l'un ou l'autre des arguments servname et nodename peut être un pointeur NULL, mais pas tous les deux. Si servname est NULL, le champ port des réponses ne sera pas initialisé (il restera égal à 0). Si nodename est NULL, l'adresse réseau dans les réponses est mis à "non initialisé" (INADDR_ANY en IPv4, IN6ADDR_ANY_INIT en IPv6) si ai_flags contient le bit AI_PASSIVE, et à l'adresse de "loopback" (INADDR_LOOPBACK ou IN6ADDR_LOOPBACK_INIT) sinon. Le cas AI_PASSIVE sert donc à obtenir des réponses utilisables par un programme serveur dans un bind pour recevoir des requêtes. Enfin si le bit AI_CANONNAME est positionné, le champ ai_canonname de la réponse contient le nom canonique de nodename.
La primitive getnameinfo remplace les primitives gethostbyaddr et getservbyport. Elle effectue la traduction d'une adresse vers un nom :
#include <sys/socket.h> #include <netdb.h> int getnameinfo(const struct sockaddr *sa, socklen_t salen, char *host, size_t hostlen, char *serv, size_t servlen, int flags);
En entrée l'argument sa pointe vers une structure d'adresse générique (de type sockaddr_in ou sockaddr_in6) et salen contient sa longueur. Le champ host (resp. serv) doit pointer sur une zone de longueur hostlen (resp. servlen) caractères. getnameinfo retourne la valeur 0 si tout est correct et un code d'erreur non nul si une erreur est détectée. S'il n'y a pas d'erreur, le champ host (resp. serv) reçoit en sortie le nom de la machine (resp. du service) correspondant. Les arguments host et serv peuvent être NULL si la réponse est inutile. Deux constantes sont définies pour permettre de réserver des zones de réponses de longueur raisonnable :
# define NI_MAXHOST 1025 # define NI_MAXSERV 32
Le champ flags permet de modifier la réponse : si flags contient le bit NI_NUMERICHOST (resp. NI_NUMERICSERV) la réponse sera l'adresse et non le nom de la machine (resp. le numéro et non le nom du service) ; si on ne sait pas trouver dans le serveur de nom le nom de la machine, getnameinfo rendra une erreur si le bit NI_NAMEREQD est positionné et l'adresse numérique sinon ; le bit NI_DGRAM indique si le service est sur UDP et non sur TCP.
Les fonctions de conversion numériques d'adresses
Elles sont l'analogue des fonctions inet_addr et inet_ntoa d'IPv4, la seule véritable différence étant qu'elles ont un argument précisant la famille d'adresse et peuvent donc aussi bien convertir les adresses IPv4 que les adresses IPv6. Comme la plupart des programmes manipulent des struct sockaddr*, il est souvent préferable d'utiliser les fonctions getaddrinfo et getnameinfo, au besoin avec le flag AI_NUMERICHOST.
#include <sys/socket.h> #include <arpa/inet.h> int inet_pton(af, src, dst) int af; /* AF_INET ou AF_INET6 */ const char *src; /* l'adresse (chaine de caract.) à traiter */ void *dst; /* le tampon où est rangé le résultat */ char * inet_ntop(af, src, dst, size) int af; /* AF_INET ou AF_INET6 */ const void *src; /* l'adresse binaire à traiter */ char *dst; /* le tampon où est rangé le résultat */ size_t size; /* la taille de ce tampon */
La primitive inet_pton convertit une adresse textuelle en sa forme binaire. Elle retourne 1 lorsque la conversion a été réussie, 0 si la chaine de caractères qui lui a été fournie n'est pas une adresse valide et -1 en cas d'erreur, c'est-à-dire lorsque la famille d'adresses (premier argument) n'est pas supportée. Actuellement, les deux seules familles d'adresses supportées sont AF_INET et AF_INET6.
La primitive duale inet_ntop convertit une adresse sous forme binaire en sa forme textuelle. Le troisième argument est un tampon destiné à recevoir le résultat de la conversion. Il doit être d'une taille suffisante, à savoir 16 octets pour les adresses IPv4 et 46 octets pour les adresses IPv6. Ces deux tailles sont définies dans le fichier netinet/in.h :
#define INET_ADDRSTRLEN 16 #define INET6_ADDRSTRLEN 46
Si la conversion est réussie, inet_ntop retourne un pointeur vers le tampon où est rangé le résultat de la conversion. Dans le cas contraire, inet_ntop retourne le pointeur nul, ce qui se produit soit lorsque la famille d'adresses n'est pas reconnue, soit lorsque la taille du tampon est insuffisante.