Je suis Charlie

Autres trucs

Accueil

Seulement les RFC

Seulement les fiches de lecture

Mon livre « Cyberstructure »

Ève

Ajouter un en-tête Destination aux paquets IPv6

Première rédaction de cet article le 11 mars 2012


Le protocole IPv6 permet de définir des options dans le paquet IPv6, options qui vont s'insérer entre l'en-tête IPv6 et les données. Comment les définir depuis son programme ? On trouve très peu d'informations à ce sujet sur le Web. Alors, voici comment je fais, en C.

Lorsqu'on voulait mettre des valeurs quelconques dans un paquet IPv4, la technique la plus utilisée était celle des « prises brutes » (raw sockets) où on peut définir chaque bit à sa convenance. Elle est mal normalisée et dangereuse (il est facile de produire des paquets invalides). IPv6 préfère donc une autre approche, normalisée dans le RFC 3542, où des primitives de plus haut niveau sont fournies pour changer des choses dans le paquet. Mais le moins qu'on puisse dire est que le RFC n'est pas spécialement convivial (pas un seul exemple) et qu'on trouve peu de tutoriels en ligne... J'avais besoin d'insérer dans les paquets une option « Destination » (RFC 8200, section 4.6) et je n'ai rien trouvé nulle part. La deuxième édition du livre de Stevens n'est guère prolixe et un des rares exemples est même carrément faux.

Bref, en repartant du RFC 3542, sections 8 et 9, voici comment insérer une option Destination dans un paquet IPv6 sortant : il existe deux méthodes, une par paquet, à base de msghdr qu'on passe à sendmsg, et une globale, affectant tous les paquets envoyés sur la prise, à base de inet6_opt_*. C'est cette dernière que j'utilise. Le principe est de constituer l'en-tête Destination à l'aide des routines inet6_opt_*, puis d'utiliser setsockopt pour l'attacher à la prise. L'en-tête sera alors ajouté à chaque paquet sortant. D'abord, quelques constantes dont on aura besoin :

#define MIN_EXTLEN 8
#define EXT_TYPE 11

Ici, MIN_EXTLEN est la longueur minimale d'un en-tête Destination, en octets (attention en lisant les paquets en binaire : le champ Hdr Ext Len de l'en-tête est en unités de 8 octets et, en prime, il part de zéro ; l'interface du RFC 3542 nous dispense heureusement de ces particularités). Quant aux options contenues dans l'en-tête Destination, on n'en mettra qu'une, de type 11, actuellement non affecté. (Notez que les deux bits de poids le plus fort encodent le comportement attendu du récepteur, s'il ne connait pas l'option. Pour 11, ces deux bits sont à zéro, indiquant au récepteur qu'il doit ignorer cette option, s'il ne la comprend pas.)

On crée ensuite la prise comme d'habitude. Puis on va fabriquer l'en-tête Destination. D'abord :

char           *ext_buffer;
ext_buffer = malloc(MIN_EXTLEN);
ext_offset = inet6_opt_init(ext_buffer, MIN_EXTLEN);
ext_buffer[0] = SOL_UDP;

Ici, ext_buffer va contenir l'option Destination. Pour l'initialiser, on indique juste la taille (en multiples de 8, obligatoirement). Rappelez-vous que la taille indiquée ici n'est pas dans les mêmes unités, ni avec le même point de départ, que ce qui apparaîtra dans le paquet sur le câble. Lisez donc bien le RFC 3542, qui normalise l'API et pas le RFC 2460 qui normalise le protocole. À ce stade, ext_buffer contient {0x11, 0x00}, où le 0x11 désigne UDP, l'en-tête qui suivra notre en-tête Destination. À chaque étape, ext_offset indiquera la taille de ext_buffer en octets

On ajoute ensuite notre option EXT_TYPE :

char           *ext_option;
uint8_t         value;
ext_option = malloc(MIN_EXTLEN);
ext_offset =
         inet6_opt_append(ext_buffer, MIN_EXTLEN, ext_offset, EXT_TYPE,
                          sizeof(value), 1,
			  &ext_option);

On a indiqué la taille de l'option (sizeof(value), notez que inet6_opt_append ajoutera tout seul la taille pour indiquer le type et la longueur, les options étant encodées en TLV) et l'alignement désiré (1, c'est-à-dire pas de contraintes). ext_option nous donnera accès au contenu de l'option. ext_buffer vaut {0x11, 0x00, 0x0b, 0x01, 0xd0} où 0x0b est le type de notre option, 0x01 sa longueur et 0xd0 une valeur quelconque puisqu'on n'a encore rien indiqué (elle va donc dépendre de votre implémentation, 0xd0 est ce que j'obtiens sur ma machine).

Maintenant, on met l'option à une certaine valeur, ici 9 :

value = 9:
inet6_opt_set_val(ext_option, 0, &value, sizeof(value)

Et c'est fait, ext_buffer vaut {0x11, 0x00, 0x0b, 0x01, 0x09}.

Si on voulait mettre plusieurs options dans l'en-tête Destination, on continuerait à appeler inet6_opt_append et inet6_opt_set_val. Mais je vais m'arrêter ici. (Notez toutefois que inet6_opt_*, plus loin, ajoutera automatiquement une option de remplissage.)

On termine le travail :

ext_offset = inet6_opt_finish(ext_buffer, MIN_EXTLEN, ext_offset);

Cet appel à inet6_opt_finish n'est pas juste pour faire joli. N'oubliez pas que l'en-tête Destination doit être composé d'un multiple de 8 octets. Et nous n'en avons que 5 (type, longueur de l'en-tête, type de l'unique option, longueur de l'unique option, valeur de l'unique option, tous à 1 octet). inet6_opt_finish va donc être chargé du remplissage (padding). Après cette fonction, ext_buffer vaudra {0x11, 0x00, 0x0b, 0x01, 0x09, 0x01, 0x01, 0x00}. Les trois derniers octets sont ceux de l'option PadN (RFC 2460, section 4.2), également en TLV : un octet de type, un de longueur et un de données.

Voilà, à ce stade, on a un ext_buffer, de longueur ext_offset, qui contient un bel en-tête Destination, avec toutes les options souhaitées, on n'a plus qu'à l'attacher à la prise :

setsockopt(sd, IPPROTO_IPV6, IPV6_DSTOPTS, ext_buffer, ext_offset);

Désormais, tout envoi de paquet par la prise sd incluera cet en-tête :

sendto(sd, message, (size_t) messagesize, 0, result->ai_addr,
               result->ai_addrlen);

Sur Linux, il faut être root malheureusement, pour ajouter l'en-tête Destination. (Emmanuel Thierry me fait remarquer que c'est un peu plus compliqué : il faut avoir le privilège CAP_NET_RAW - requis pour ouvrir une prise brute, pas nécessairement être root. Donc on peut suggérer plutôt l'utilisation de setcap 'CAP_NET_RAW+eip' /chemin/vers/le/binaire. Ou bien démarrer en root, et dès le démarrage du programme ne garder que ce privilège et abandonner le reste.)

On peut voir le résultat avec tcpdump :

15:31:41.020647 IP6 (hlim 64, next-header unknown (60) payload length: 26) \
    2a01:e35:8bd9:8bb0:a0a7:ea9c:74e8:d397 > 2001:4b98:dc0:41:216:3eff:fece:1902: \
    DSTOPT (opt_type 0x0b: len=1) (padn) \
    42513 > 42: [udp sum ok] UDP, length 10

Le next-header unknown est l'en-tête Destination, qui a en effet le numéro 60. tcpdump a quand même reconnu cet en-tête DSTOPT et vu l'option de type 11 et le PadN. Ensuite, il a bien vu l'UDP vers le port 42. Avec tshark, c'est moins bien :

Internet Protocol Version 6
    0110 .... = Version: 6
        [0110 .... = This field makes the filter "ip.version == 6" possible: 6]
    .... 0000 0000 .... .... .... .... .... = Traffic class: 0x00000000
    .... .... .... 0000 0000 0000 0000 0000 = Flowlabel: 0x00000000
    Payload length: 26
    Next header: IPv6 destination option (0x3c)
    Hop limit: 64
    Source: 2a01:e35:8bd9:8bb0:a0a7:ea9c:74e8:d397 (2a01:e35:8bd9:8bb0:a0a7:ea9c:74e8:d397)
    Destination: 2001:4b98:dc0:41:216:3eff:fece:1902 (2001:4b98:dc0:41:216:3eff:fece:1902)
    Destination Option 
        Next header: UDP (0x11)
        Length: 0 (8 bytes)
User Datagram Protocol, Src Port: 42513 (42513), Dst Port: name (42)
    Source port: 42513 (42513)
    Destination port: name (42)
    Length: 18

tshark a tout juste trouvé qu'il y avait un en-tête Destination de longueur 8, sans pouvoir l'analyser. Wireshark affiche au moins le contenu de l'option, en hexadécimal.

Si vous aimez lire le C, voici le programme que j'ai utilisé.

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)