Programmation d'applications bis

From Livre IPv6

Revision as of 13:42, 30 June 2009 by Eduble (Talk | contribs)

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

CS191.gif

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.

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

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

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

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

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

Exemple de client/serveur TCP

Vue d'ensemble

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

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

Il se compose de cinq fichiers sources :

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

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

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

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

CS192.gif

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

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

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

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

#define SERVICE "nbus"

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

nbus 20000/tcp

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

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

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

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


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

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

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

CS193.gif

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

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


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

Le serveur

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

CS194.gif


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


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

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

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


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

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

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


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

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

Personal tools