Je suis Charlie

Autres trucs

Accueil

Seulement les RFC

Seulement les fiches de lecture

Ève

Les structures de données IP en C

Première rédaction de cet article le 5 mars 2009
Dernière mise à jour le 6 mars 2009


Comme je ne programme pas en C tous les jours, à chaque fois que je m'y remets (et c'est en général pour un programme réseau), j'ai du mal avec les structures de données qui servent à IP. Alors, pour garder trace de ce que j'ai compris la dernière fois, le plus simple est encore d'en faire profiter l'univers en mettant quelques notes sur ce blog.

Les structures de données qui stockent les adresses IP, les métadonnées sur ces adresses comme leur famille (IPv4 ou IPv6), les prises, etc, sont complexes. Mais c'est le résultat d'un choix de faire assez général, notamment pour permettre au même programme de gérer IPv4 et IPv6, avec le minimum de modifications.

Donc, la structure de données la plus importante est la struct addrinfo. Elle stocke tout ce qu'il faut pour se connecter à une machine, notamment son adresse IP. Pour pouvoir être chaînée à d'autres struct addrinfo, elle se termine par un pointeur vers la structure suivante. Sa définition officielle est dans le RFC 3493 et on trouve le code (un peu plus complexe qu'ici) dans /usr/include/netdb.h :

struct addrinfo {
        int             ai_flags;       /* AI_PASSIVE, AI_CANONNAME */
        int             ai_family;      /* PF_xxx */
        int             ai_socktype;    /* SOCK_xxx */
        int             ai_protocol;    /* 0 or IPPROTO_xxx for IPv4 and IPv6 */
        socklen_t        ai_addrlen;    /* length of ai_addr */
        char            *ai_canonname;  /* canonical name for hostname */
        struct sockaddr *ai_addr;       /* binary address */
        struct addrinfo *ai_next;       /* next structure in linked list */
};

On voit donc que les adresses sont de type struct sockaddr. Ce type est défini dans /usr/include/sys/socket.h (parfois via une inclusion d'un autre fichier) et vaut :

struct sockaddr {
        uint8_t       sa_len;         /* total length */
        sa_family_t     sa_family;      /* address family */
        char            sa_data[14];    /* address value */
};

Le point important est que c'est une structure générique, qui marche aussi bien pour IPv4 que pour IPv6. Pour extraire des informations pertinentes à ces deux protocoles, il faut convertir les struct sockaddr génériques en struct sockaddr_in et struct sockaddr_in6 spécifiques. Cela se fait en général en convertissant les pointeurs (exemple ci-dessous lors de l'utilisation de inet_ntop()).. La structure pour IPv6 vaut (/usr/include/netinet/in.h) :

struct sockaddr_in6 {
    uint8_t         sin6_len;       /* length of this struct */
    sa_family_t     sin6_family;    /* AF_INET6 */
    in_port_t       sin6_port;      /* transport layer port # */
    uint32_t        sin6_flowinfo;  /* IPv6 flow information */
    struct in6_addr sin6_addr;      /* IPv6 address */
    uint32_t        sin6_scope_id;  /* set of interfaces for a scope */
}; 

et celle pour IPv4 :

struct sockaddr_in {
        uint8_t         sin_len;
        sa_family_t     sin_family;
        in_port_t       sin_port;
        struct in_addr  sin_addr;
        __int8_t        sin_zero[8];
};

Notez la padding de huit octets à la fin, qui lui permet d'avoir exactement la même taille que son équivalente IPv6. Normalement, ce n'est pas garanti, si on veut une structure de données qui puisse réellement contenir les deux familles d'adresses, on devrait utiliser struct sockaddr_storage (RFC 3493, section 3.10).

Nous nous approchons désormais sérieusement des adresses IP tout court. Il reste une étape, regarder le champ sin_addr ou sin6_addr. Il est de type struct in_addr ou struct in6_addr. La définition de ces structures est également dans /usr/include/netinet/in.h (in6_addr est présentée à la section 3.2 du RFC 3493) :

  
struct in_addr {
    uint32_t s_addr;
  };
...
struct in6_addr {
          uint8_t  s6_addr[16];     
};

Les deux structures stockent la représentation binaire des adresses IP, celle qui circule sur le câble : quatre octets pour IPv4 et 16 pour IPv6. Si vous regardez le fichier .h, vous verrez que in6_addr est souvent représentée sous forme d'une union, pour faciliter l'alignement.

Maintenant qu'on a ces structures de données, comment les utilise t-on ? Pour remplir les struct addrinfo, on utilise getaddrinfo() qui a remplacé gethostbyname() il y a de très nombreuses années. Par exemple, si le nom de la machine qu'on cherche est dans server (un simple char *) :

/* hints and res are "struct addrinfo", the first one is an IN
parameter, the second an OUT one. */
memset(&hints, 0, sizeof(hints));
hints.ai_family = PF_UNSPEC; /* v4 or v6, I don't care */
hints.ai_socktype = SOCK_STREAM;
getaddrinfo(server, port_name, &hints, &res);
/* Now, "res" contains the IP addresses of "server" */
/* We can now do other things such as creating a socket: */
sockfd = socket(res->ai_family, res->ai_socktype, protocol);
/* And connecting to it, since connect() takes a "struct sockaddr" as input: */
connect(sockfd, res->ai_addr, res->ai_addrlen);

Notez que le code ci-dessus ne parcourt par la liste des adresses renvoyée par getaddrinfo(), seule la première est utilisée (ai_next est ignorée).

Dans la struct addrinfo passée en premier paramètre à getaddrinfo(), une option est particulièrement intéressante, AI_NUMERICHOST qui permet de s'assurer qu'une chaîne de caractères, donnée par l'utilisateur, a bien la syntaxe d'une adresse IP :

hints_numeric.ai_flags = AI_NUMERICHOST;
error = getaddrinfo(server, port_name, &hints_numeric, &res);
if (error && error == EAI_NONAME) { /* It may be a name, but
it is certainly not an address */
... /* Handle it as a name */

Et pour afficher des adresses à l'utilisateur humain ? Les structures in_addr et in6_addr stockent la forme binaire de l'adresse mais, ici, on veut la forme texte. Pour cela, deux fonctions sont fournies, inet_ntop() pour traduire l'adresse en texte et inet_pton() pour le contraire. Supposons que la variable myaddrinfo soit une struct addrinfo, afficher la première adresse IP nécessite d'extraire ai_addr, de le convertir dans la struct sockaddr du bon type (struct sockaddr_in6 pour IPv6) et d'appeler inet_ntop() avec les bons paramètres, qui dépendent de la famille :

struct addrinfo *myaddrinfo;
struct sockaddr *addr;
struct sockaddr_in6 *addr6;
addr = myaddrinfo->ai_addr;
char *address_text;
...
if (myaddrinfo->ai_family == AF_INET6) {
       address_text = malloc(INET6_ADDRSTRLEN);
       addr = myaddrinfo->ai_addr;
       addr6 = (struct sockaddr_in6 *) addr; /* Conversion between the
                      various  struct sockaddr* is always possible,
		      since they have the same layout */
       inet_ntop (AF_INET6, &addr6->sin6_addr, address_text,
                         INET6_ADDRSTRLEN);
}
else /* IPv4... */

Un descripteur de prise, lui, est très simple, c'est un bête entier :

int             sockfd;
...
sockfd = socket(...);

Il pointe, via la table des descripteurs (que vous ne manipulez jamais) vers la vraie prise. C'est toujours le descripteur qu'on passera comme paramètre pour agir sur la prise, par exemple pour la lier à une adresse donnée :

bind(sockfd, ...);

L'article IP Addresses, structs, and Data Munging m'a beaucoup aidé et fournit une excellente introduction. Pour tout connaître en détail, la référence est bien sûr le livre de Stevens, Unix Network Programming. Merci à Samuel Tardieu pour ses remarques sur la conversion des struct sockaddr.

Version PDF de cette page (mais vous pouvez aussi imprimer depuis votre navigateur, il y a une feuille de style prévue pour cela)

Source XML de cette page (cette page est distribuée sous les termes de la licence GFDL)