Mini-ping
Un article de IPv6.
Exemple de client/serveur TCP | Table des matières | Utilisation du multicast |
Sommaire |
Description
La commande proposée est une version très simplifiée de ping6. Néanmoins, cela permettra de comprendre l'essentiel du fonctionnement de cette commande. Son principe est le suivant, on émet un paquet ICMPv6 du type ECHO_REQUEST et on active une temporisation. Si, le délai étant expiré, on n'a pas reçu de paquet ICMPv6 de type ECHO_REPLY en provenance de la machine cible, on imprime un message d'erreur. Dans le cas contraire, on imprime le nom de la machine émettrice de l'ECHO_REPLY. Par exemple, si le nom donné à cette commande est one_ping6 :
$ one_ping6 peirce Sending ECHO REQUEST to: peirce.ipv6.logique.jussieu.fr Waiting for answer (timeout = 5s)... Got answer from 2001:660:101:201:200:f8ff:fe31:1942 (seq = 0) $
Remarque : ICMP étant un protocole non fiable, il peut arriver qu'un premier paquet soit perdu, par exemple à cause du temps passé à exécuter le protocole de "recherche de voisins". Il suffit en général de relancer la commande pour que la réponse apparaisse la seconde fois.
one_ping6 accepte les options suivantes :
- -d données. Ces données seront incluses dans le paquet ECHO_REQUEST.
- -s numéro de séquence. La valeur défaut est zéro.
- -t durée de la temporisation. La valeur par défaut est fixée lors de la compilation via la macro-définition TIMEOUT.
Par exemple,
$ one_ping6 -d 'Un petit essai' -s 12 -t 3 peirce Sending ECHO REQUEST to: peirce.ipv6.logique.jussieu.fr Waiting for answer (timeout = 3s)... Got answer from 2001:660:101:201:200:f8ff:fe31:1942 (seq = 12) with data [ Un petit essai ] (end of data) $
Les sources de ce programme se composent de trois fichiers : le programme principal, le source de la fonction assurant l'émission du paquet ECHO_REQUEST et le source de la fonction ayant en charge la gestion de la temporisation et la réception du paquet ECHO_REPLY.
Envoi du paquet ECHO_REQUEST
Rappelons tout d'abord que le nouveau protocole ICMPv6 est une refonte presque complète d'ICMP (sur IPv4). Néanmoins, le format des paquets ECHO_REQUEST et ECHO_REPLY est inchangé excepté la valeur du champ type (cf. figure format d'un message ICMPv6 demande et réponse d'écho).
La préparation d'un paquet ECHO_REQUEST est similaire en ICMP(v4) ou ICMPv6. La seule différence est que le calcul du checksum n'est maintenant plus à la charge du programmeur mais effectué par le noyau. Plus précisément, ainsi qu'il est spécifié dans l'API "avancée", pour toutes les sockets de type SOCK_RAW et de protocole IPPROTO_ICMPV6, c'est le noyau qui doit calculer le checksum des paquets ICMPv6 sortants (dans le cas des Linux anciens, il faut activer le calcul du checksum, comme on le voit en lignes 81 à 95 du fichier ping.c ).
Le paquet ICMPv6 de type ECHO_REQUEST, étant ainsi constitué, on l'expédie, via la primitive sendto à la machine cible.
1| #include <stdio.h> 2| #include <string.h> 3| #include <sys/types.h> 4| #include <sys/socket.h> 5| #include <netinet/in.h> 6| #include <netinet/ip6.h> 7| #include <netinet/icmp6.h> 8| #include <arpa/inet.h> 9| #include <netdb.h> 10| 11| #ifndef MAX_DATALEN 12| #define MAX_DATALEN (1280 - sizeof(struct ip6_hdr) - sizeof(struct icmp6_hdr)) 13| #endif 14| 15| static u_char buf[sizeof(struct icmp6_hdr) + MAX_DATALEN]; 16| 17| int send_echo_request6(int sock, struct sockaddr_in6 *dst, uint16_t id, 18| uint16_t seq, char *opt_data, int opt_data_size) 19| { 20| int noc, icmp_pkt_size = sizeof(struct icmp6_hdr); 21| struct icmp6_hdr *icmp; 22| 23| if (opt_data && opt_data_size > MAX_DATALEN) { 24| fprintf(stderr, "send_echo_request6: too much data (%d > %d)\n", 25| opt_data_size, MAX_DATALEN); 26| return -1; 27| } 28| 29| memset((void *) buf, 0, sizeof(buf)); 30| icmp = (struct icmp6_hdr *) buf; 31| icmp->icmp6_type = ICMP6_ECHO_REQUEST; 32| icmp->icmp6_id = id; 33| icmp->icmp6_seq = seq; 34| if (opt_data) { 35| memcpy(buf + sizeof(struct icmp6_hdr), opt_data, opt_data_size); 36| icmp_pkt_size += opt_data_size; 37| } 39| noc = sendto(sock, (char *) icmp, icmp_pkt_size, 0, 39| (struct sockaddr *) dst, sizeof(struct sockaddr_in6)); 40| if (noc < 0) { 41| perror("send_echo_request6: sendto"); 42| return -1; 43| } 44| if (noc != icmp_pkt_size) { 45| fprintf(stderr, "send_echo_request6: wrote %d bytes, ret=%d\n", 46| icmp_pkt_size, noc); 47| return -1; 48| } 49| return 0; 50| }
Une dernière remarque avant de clore cette section. On a vu que l'on pouvait inclure des données dans le paquet ICMPv6 émis. La taille maximale de celles-ci a été choisie (ligne 12) pour que les paquets ne soient jamais fragmentés (le protocole IPv6 exigeant une taille de paquet minimale de 1280 octets, en-têtes comprises). Une taille plus grande serait possible, les paquets ICMP ECHO pouvant parfaitement être fragmentés.
La réception du paquet ECHO_REPLY
C'est la fonction wait_for_echo_reply6 qui gère la réception du paquet ECHO_REPLY. Cette fonction tout d'abord (lignes 32 à 35) utilise le mécanisme de filtrage des paquets ICMPv6, mécanisme défini dans l'API "étendue", afin que seuls les paquets ICMPv6 de type ECHO_REPLY soient reçus sur la socket d'écoute.
On trouve ensuite une boucle sans fin dont on sort soit sur réception du signal SIGALRM (armé juste avant l'entrée de la boucle à la ligne 36), c'est-à-dire lorsque le délai de temporisation (argument timeout) est expiré, soit lorsque la fonction recv_icmp_pkt, qui analyse tous les paquets ICMPv6 de type ECHO_REPLY reçus sur la socket d'écoute (argument sock) par l'émetteur, retourne 0, c'est-à-dire lorsque le paquet ECHO_REPLY en provenance de la machine cible a été détecté.
1| #include <stdio.h> 2| #include <unistd.h> 3| #include <string.h> 4| #include <sys/types.h> 5| #include <sys/socket.h> 6| #include <netinet/in.h> 7| #include <netinet/ip6.h> 8| #include <netinet/icmp6.h> 9| #include <arpa/inet.h> 10| #include <errno.h> 11| #include <signal.h> 12| #include <setjmp.h> 13| 14| #ifndef MAX_DATALEN 15| #define MAX_DATALEN (1280 - sizeof(struct ip6_hdr) - sizeof(struct icmp6_hdr)) 16| #endif 17| 18| static void on_timeout(int); 19| static int recv_icmp_pkt(int, struct sockaddr_in6 *, uint16_t, uint16_t); 20| 21| static u_char buf[sizeof(struct icmp6_hdr) + MAX_DATALEN]; 22| static jmp_buf j_buf; 23| 24| void wait_for_echo_reply6(int sock, struct sockaddr_in6 *from, uint16_t id, 25| uint16_t seq, int timeout) 26| { 27| struct icmp6_filter filter; 28| char from_ascii[INET6_ADDRSTRLEN]; 29| 30| inet_ntop(AF_INET6, &from->sin6_addr, from_ascii, INET6_ADDRSTRLEN); 31| 32| ICMP6_FILTER_SETBLOCKALL(&filter); 33| ICMP6_FILTER_SETPASS(ICMP6_ECHO_REPLY, &filter); 34| setsockopt(sock, IPPROTO_ICMPV6, ICMP6_FILTER, (const void *) &filter, 35| sizeof(filter)); 36| signal(SIGALRM, on_timeout); 37| alarm(timeout); 38| for (;;) { 39| int noc, from_len = sizeof(struct sockaddr_in6); 40| 41| if (setjmp(j_buf) == SIGALRM) { 42| fprintf(stderr, "No answer from %s\n", from_ascii); 43| break; 44| } 45| noc = recvfrom(sock, buf, sizeof(buf), 0, 46| (struct sockaddr *) from, &from_len); 47| if (noc < 0) { 48| if (errno == EINTR) 49| continue; 50| perror("wait_for_echo_reply6: recvfrom"); 51| continue; 52| } 53| if (recv_icmp_pkt(noc, from, id, seq) == 0) 54| break; 55| } 56| alarm(0); 57| signal(SIGALRM, SIG_DFL); 58| return; 59| } 60| 61| static void on_timeout(int sig) 62| { 63| longjmp(j_buf, sig); 64| }
Contrairement à ce qui se passait en IPv4, l'entête IPv6 n'est pas incluse lors de la réception d'un paquet ICMPv6 (sauf si l'option IP_HDRINCL est positionnée). Ainsi dans la fonction recv_icmp_pkt, on commence directement par tester le champ identificateur et le numéro de séquence (lignes 84 et 85). Si ce test a été passé avec succès, c'est-à-dire que l'on a bien reçu le paquet attendu, la fonction recv_icmp_pkt retourne 0 après avoir, s'il y en a, imprimé les données incluses dans le paquet. Dans le cas contraire, la valeur retournée est 1.
65| static int recv_icmp_pkt(int noc, struct sockaddr_in6 *from, uint16_t id, 66| uint16_t seq) 67| { 68| int opt_data_size; 69| char from_ascii[INET6_ADDRSTRLEN]; 70| struct icmp6_hdr *icmp; 71| 72| if (inet_ntop(AF_INET6, &from->sin6_addr, from_ascii, 73| INET6_ADDRSTRLEN) == NULL) { 74| perror("inet_ntop"); 75| return -1; 76| } 77| if (noc < sizeof(struct icmp6_hdr)) { 78| fprintf(stderr, "recv_icmp_pkt: packet too short from %s\n", 79| from_ascii); 80| return -1; 81| } 82| opt_data_size = noc - sizeof(struct icmp6_hdr); 83| icmp = (struct icmp6_hdr *) buf; 84| if (icmp->icmp6_id != id || icmp->icmp6_seq != seq) 85| return 1; 86| fprintf(stdout, "Got answer from %s (seq = %d)\n", from_ascii, seq); 87| if (opt_data_size > 0) { 88| fprintf(stdout, "with data [\n"); 89| fflush(stdout); 90| if (opt_data_size > MAX_DATALEN) { 91| fprintf(stderr, 92| "recv_icmp_pkt: received too much data from %s\n", 93| from_ascii); 94| } 95| else 96| write(1, (char *) icmp + sizeof(struct icmp6_hdr), opt_data_size); 97| fprintf(stdout, "\n] (end of data)\n"); 98| } 99| return 0; 100| }
Programme principal
Le programme principal ne présente pas de difficulté particulière puisqu'il est une application directe des fonctions décrites dans les deux sections précédentes.
La première partie est triviale : elle concerne le traitement des (éventuelles) options.
1| #include <stdio.h> 2| #include <stdlib.h> 3| #include <unistd.h> 4| #include <string.h> 5| #include <sys/socket.h> 6| #include <netinet/in.h> 7| #include <netinet/icmp6.h> 8| #include <arpa/inet.h> 9| #include <netdb.h> 10| #ifdef __linux__ 11| #include <linux/version.h> 12| #if LINUX_VERSION_CODE < KERNEL_VERSION(2,4,19) 13| #define LINUX_CKSUM_CALCUL_EXPLICITE 14| #endif 15| #endif 16| 17| #ifndef TIMEOUT 18| #define TIMEOUT 5 19| #endif 20| 21| extern int send_echo_request6(int, struct sockaddr_in6 *, uint16_t, 22| uint16_t, char *, int); 23| extern void wait_for_echo_reply6(int, struct sockaddr_in6 *, uint16_t, 24| uint16_t, int); 25| 26| static void usage(char *); 27| 28| int main(int argc, char **argv) 29| { 30| int sock, timeout = TIMEOUT, a, ecode; 31| char *opt_data = NULL, *dst_ascii; 32| int opt_data_size = 0; 33| uint16_t id, seq = 0; 34| struct sockaddr_in6 *dst; 35| struct addrinfo *res; 36| struct addrinfo hints = { 37| AI_CANONNAME, 38| PF_INET6, 39| SOCK_RAW, 40| IPPROTO_ICMPV6, 41| 0, 42| NULL, 43| NULL, 44| NULL 45| }; 46| 47| while((a = getopt(argc, argv, "d:s:t:")) != EOF) 48| switch(a) { 49| case 'd': 50| opt_data = optarg; 51| opt_data_size = strlen(optarg) + 1; 52| break; 53| case 's': 54| seq = (uint16_t) atoi(optarg); 55| break; 56| case 't': 57| timeout = atoi(optarg); 58| break; 59| default: 60| usage(*argv); 61| } 62| argc -= optind; 63| if (argc != 1) 64| usage(*argv); 65| argv += optind;
Ensuite c'est la préparation de l'adresse de la socket distante, opération qui est devenue maintenant familière. Noter que l'on a affecté au champ ai_family de la structure hints la valeur PF_INET6 lors de sa déclaration (ligne 38) : on doit s'assurer que la machine cible est une machine IPv6 (il n'existe pas de mode double pile avec utilisation d'adresse IPv4 mappé pour le protocole ICMP, car celui-ci a fortement changé entre IPv4 et IPv6). On s'est interdit des adresses destination de type multicast (lignes 73 à 76) car, comme l'on ne traite qu'un paquet en réception, cela n'aurait guère d'intérêt.
On crée la socket qui servira à l'émission du paquet ECHO_REQUEST et à la réception du paquet ECHO_REPLY en provenance de la machine cible.
À la ligne 96, la valeur du champ identificateur du paquet ICMPv6 est calculée en fonction du numéro de processus en prenant les 16 premiers bits. C'est une technique sûre (et simple) quant à la garantie de l'unicité de l'identificateur. Enfin le paquet ECHO_REQUEST est émis (send_echo_request6) puis on attend la réponse éventuelle (wait_for_echo_reply6).
66| ecode = getaddrinfo(*argv, NULL, &hints, &res); 67| if (ecode) { 68| fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ecode)); 69| exit(1); 70| } 71| dst_ascii = res->ai_canonname ? res->ai_canonname : *argv; 72| dst = (struct sockaddr_in6 *) res->ai_addr; 73| if (IN6_IS_ADDR_MULTICAST(&dst->sin6_addr)) { 74| fprintf(stderr, "%s multicast address not supported\n", dst_ascii); 75| exit(1); 76| } 77| if ((sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol)) < 0) { 78| perror("socket (RAW)"); 79| exit(1); 80| } 81| #ifdef LINUX_CKSUM_CALCUL_EXPLICITE 82| { 83| /* 84| * Pour linux avant 2.4.19, il faut demander le calcul des checksums 85| * sur les sockets raw, meme pour des paquets icmpv6 86| */ 87| #define OFFSETOF(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) 88| int off = OFFSETOF(struct icmp6_hdr, icmp6_cksum); 89| 90| if (setsockopt(sock, SOL_RAW, IPV6_CHECKSUM, &off, sizeof off) < 0) { 91| perror("setsockopt (IPV6_CHECKSUM)"); 92| exit(1); 93| } 94| } 95| #endif 96| id = (uint16_t) (getpid() & 0xffff); 97| fprintf(stdout, "Sending ECHO REQUEST to: %s\n", dst_ascii); 98| if (send_echo_request6(sock, dst, id, seq, opt_data, 99| opt_data_size) < 0) 100| exit(1); 101| fprintf(stdout, "Waiting for answer (timeout = %ds)...\n", timeout); 102| wait_for_echo_reply6(sock, dst, id, seq, timeout); 103| close(sock); 104| exit(0); 105| } 106| 107| static void usage(char *s) 108| { 109| fprintf(stderr, "Usage: %s [-d data] [-s seq] [-t timeout] host | addr\n", s); 110| exit(1); 111| }
Exemple de client/serveur TCP | Table des matières | Utilisation du multicast |