Je suis Charlie

Autres trucs

Accueil

Seulement les RFC

Seulement les fiches de lecture

Mon livre « Cyberstructure »

Ève

Programmation réseau avec des prises brutes, en IPv4 et en IPv6

Première rédaction de cet article le 16 avril 2009
Dernière mise à jour le 20 avril 2009


De nombreuses applications réseau nécessitent de contrôler étroitement le contenu du paquet IP, y compris les parties qui ne sont normalement pas accessibles aux applications. Cela peut se faire en C avec des appels adéquats à setsockopt() mais, si on veut vraiment changer la plus grande partie du paquet, il peut être plus simple de fabriquer un paquet IP entièrement « à la main » en utilisant des prises brutes (en anglais raw sockets). Voyons comment on fait en IPv4 et si c'est possible en IPv6.

Un petit avertissement, cher lecteur, ce qui suit est d'assez bas niveau, au sens de « proche de la machine ». L'API des prises offre des mécanismes de programmation bien plus simples que les prises brutes. Même si on veut changer une partie de l'en-tête, par exemple le champ TTL, il existe des techniques plus simples à base de setsockopt(). Avec les prises brutes, au contraire, le noyau ne fera plus grand'chose pour vous, vous allez devoir vous débrouiller seul (prononcer le tout avec une voix sépulcrale). Considérez vous mis en garde.

D'abord, le principe, tel qu'illustré depuis de nombreuses années par les applications qui ont besoin d'accéder à ces fonctions de bas niveau, comme traceroute. On déclare la prise de type SOCK_RAW, de protocole IPPROTO_RAW et, pour bien dire au noyau qu'on se charge de tout, on utilise l'option IP_HDRINCL de setsockopt :

int             on = 1;
...
sd = socket(result->ai_family, SOCK_RAW, IPPROTO_RAW);
setsockopt(sd, IPPROTO_IP, IP_HDRINCL, (char *) &on, sizeof(on));

Ensuite, on crée une structure pour représenter le paquet IP (ici, on utilisera UDP) :

struct opacket4 {
    struct ip       ip; /* 'struct ip' est une représentation en C
    d'un paquet IP, déclarée dans <netinet/ip.h> */
    struct udphdr   udp;
    char            payload[MSGSIZE];
} __attribute__ ((packed)); /* La directive 'packed' s'assure que le
compilateur ne laissera pas d'espace entre les champs */
...
struct opacket4 op4; /* Output Packet of IP version 4 */
/* S'assurer que tout est à zéro au début */
memset(&op4.ip, '\0', sizeof(op4.ip));

Enfin, on peuple cette structure à sa guise, en lisant le RFC 791 pour savoir ce qu'il faut mettre :

op4.ip.ip_v = 4; /* Version */
op4.ip.ip_hl = sizeof(op4.ip) >> 2; /* Taille des en-têtes (en IPv4,
                                       elle est variable) */
op4.ip.ip_dst = ... (un champ sin_addr...)
/* Sur Linux, l'adresse source, op4.ip.ip_src, est mise
automatiquement, même pour une prise brute */
op4.ip.ip_p = SOL_UDP; /* Protocole de transport utilisé */
op4.ip.ip_ttl = 1; /* Limite à un seul saut */
headersize = sizeof(op4.ip) + sizeof(op4.udp);
packetsize = headersize + strlen(message);
op4.ip.ip_len = htons(packetsize);

On fait pareil pour la partie UDP et on peut alors envoyer le paquet :

sendto(sd, &op4, packetsize, 0, result->ai_addr,
               result->ai_addrlen);

Le programme complet qui met en œuvre cette technique s'utilise ainsi :

# ./send-udp -v --port 5354 192.168.2.1 
Connecting to 192.168.2.1...
Sending 42 bytes (28 for the headers)...

et, vu avec tcpdump, cela donne bien le résultat attendu :

13:55:12.241391 IP (tos 0x0, ttl 1, id 44977, offset 0, flags [none], \
     proto UDP (17), length 42) 192.168.2.2.5354 > 192.168.2.1.5354: \
     [no cksum] UDP, length 14

L'adresse source a été mise seule mais le programme a une option pour la forcer :

# ./send-udp -v --port 5354 --source 192.0.2.1 192.168.2.1 
Connecting to 192.168.2.1...
Sending 42 bytes (28 for the headers)...

qui donne :

13:55:24.815620 IP (tos 0x0, ttl 23, id 45015, offset 0, flags [none], \
     proto UDP (17), length 42) 192.0.2.1.5354 > 192.168.2.1.5354: \
     [no cksum] UDP, length 14

Il existe quelques pièges, par exemple le fait que la somme de contrôle de l'en-tête IP (cf. RFC 791) n'est pas forcément remplie automatiquement (ça dépend du noyau utilisé) et qu'il peut donc être nécessaire de la calculer soi-même. Je l'ai dit, avec les prises brutes, il faut penser à tout.

OK, ça, c'était pour IPv4. Et en IPv6 ? Eh bien, c'est plus compliqué. Pour des raisons exposées dans la section 1 du RFC 3542, l'option IP_HDRINCL n'est pas garantie en IPv6. La démarche « normale » est d'utiliser les techniques dudit RFC, à base de setsockopt et de renoncer aux prises brutes (attention, le RFC utilise, en sa section 3, le terme de raw sockets quand même, pour désigner les prises où un appel à setsockopt a permis de modifier certains champs). (La section 5 du RFC 3542 propose une autre approche dite d'ancillary data.)

Si on veut quand même faire de la prise brute en IPv6, il n'y a que des solutions non portables. On peut se servir du BPF (où on peut modifier tout ce qui est au dessus de la couche 2, option PF_PACKET de socket(), cf. packet(7)) ou bien utiliser les prises brutes comme en IPv4, sur les systèmes où ça marche, typiquement Linux.

(Mathieu Peresse me fait remarquer que, sur Linux, « après examen du code du noyau, pour IPv4, créer une RAW socket IPv4 avec IPPROTO_RAW comme protocole équivaut exactement à créer une socket RAW IPv4 et mettre l'option IP_HDRINCL à 1 (la fonctionnalité d'inclure le header est en fait activée en mettant le membre "hdrincl" de la structure "inet_sock" à 1)... ».)

Sur FreeBSD et NetBSD, IP_HDRINCL ne marche tout simplement pas (cf. http://lists.freebsd.org/pipermail/freebsd-pf/2006-May/002174.html).

Le programme indiqué plus haut marche donc également en IPv6 mais sur Linux uniquement. La prise est créée avec exactement les mêmes options qu'en v4, la configuration en vraie prise brute est légèrement différente, on n'utilise pas IP_HDRINCL, juste le type IPPROTO_RAW. Et les structures de données sont proches :

struct opacket6 {
    struct ip6_hdr  ip;
    struct udphdr   udp;
    char            payload[MSGSIZE];
} __attribute__ ((packed));
struct opacket6 op6;

sauf que cette fois, il faut lire le RFC 2460 en les remplissant (des champs comme Protocol ou TTL n'ont pas le même nom, car leur sémantique est légèrement différente) :

memset(&op6.ip, '\0', sizeof(op6.ip));
op6.ip.ip6_vfc = 6 << 4; /* Les quatre premiers bits stockent la
                                  version, ici 6 */
op6.ip.ip6_dst = sockaddr6->sin6_addr;
op6.ip.ip6_nxt = SOL_UDP;
op6.ip.ip6_hlim = TTL;
headersize = sizeof(op6.ip) + sizeof(op6.udp);
packetsize = headersize + strlen(message);
op6.ip.ip6_plen = htons((uint16_t) packetsize);

Par défaut, si la machine visée a des adresses v4 et v6, il utilise la politique de préférence du système (celle qui, sur Linux, se règle dans /etc/gai.conf) , mais on peut la forcer avec les options -4 et -6. tcpdump voit :

14:07:52.030339 ::.5354 > 2a01:e35:8bd9:8bb0:a00:20ff:fe99:faf4.5354: \
               UDP, length: 14 (len 62, hlim 1)

On note que l'adresse source n'a pas été mise automatiquement, en IPv6. Elle reste donc à zéro (::, qui veut dire « adresse indéfinie »). Si on la force :

14:08:29.745203 2001:db8::1.5354 > 2a01:e35:8bd9:8bb0:a00:20ff:fe99:faf4.5354: \
              UDP, length: 14 (len 62, hlim 1)

Merci à Mathieu Peresse et à Yves Legrand-Gérard pour leur aide. Outre le RFC, on peut aussi trouver une bonne explication en français de l'API dans le livre de Gisèle Cizault.

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)