Difference between revisions of "Mini-ping"

From Livre IPv6

 
(Envoi du paquet ECHO_REQUEST)
Line 54: Line 54:
 
    
 
    
 
  static u_char buf[sizeof(struct icmp6_hdr) + MAX_DATALEN];  
 
  static u_char buf[sizeof(struct icmp6_hdr) + MAX_DATALEN];  
 
+
 
  int send_echo_request6(int sock, struct sockaddr_in6 *dst, uint16_t id,
 
  int send_echo_request6(int sock, struct sockaddr_in6 *dst, uint16_t id,
 
                         uint16_t seq, char *opt_data, int opt_data_size)
 
                         uint16_t seq, char *opt_data, int opt_data_size)
Line 60: Line 60:
 
  int noc, icmp_pkt_size = sizeof(struct icmp6_hdr);
 
  int noc, icmp_pkt_size = sizeof(struct icmp6_hdr);
 
  struct icmp6_hdr *icmp;   
 
  struct icmp6_hdr *icmp;   
 
+
 
     if (opt_data && opt_data_size > MAX_DATALEN) {
 
     if (opt_data && opt_data_size > MAX_DATALEN) {
 
       fprintf(stderr, "send_echo_request6: too much data (%d > %d)\n",
 
       fprintf(stderr, "send_echo_request6: too much data (%d > %d)\n",

Revision as of 16:16, 17 December 2005

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. See Rappel du 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 sortants50.

Le paquet ICMPv6 de type ECHO_REQUEST, étant ainsi constitué, on l'expédie, via la primitive sendto à la machine cible.


#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip6.h>
#include <netinet/icmp6.h>
#include <arpa/inet.h>
#include <netdb.h>
 
#ifndef MAX_DATALEN
#define MAX_DATALEN (1280 - sizeof(struct ip6_hdr) - sizeof(struct icmp6_hdr))
#endif
 
static u_char buf[sizeof(struct icmp6_hdr) + MAX_DATALEN]; 

int send_echo_request6(int sock, struct sockaddr_in6 *dst, uint16_t id,
                       uint16_t seq, char *opt_data, int opt_data_size)
{
int noc, icmp_pkt_size = sizeof(struct icmp6_hdr);
struct icmp6_hdr *icmp;   

   if (opt_data && opt_data_size > MAX_DATALEN) {
      fprintf(stderr, "send_echo_request6: too much data (%d > %d)\n",
      opt_data_size, MAX_DATALEN);
      return -1;
   }

   memset((void *) buf, 0, sizeof(buf));
   icmp = (struct icmp6_hdr *) buf;
   icmp->icmp6_type = ICMP6_ECHO_REQUEST;
   icmp->icmp6_id = id;
   icmp->icmp6_seq = seq;
   if (opt_data) {
      memcpy(buf + sizeof(struct icmp6_hdr), opt_data, opt_data_size);
      icmp_pkt_size += opt_data_size;
   } 
   noc = sendto(sock, (char *) icmp, icmp_pkt_size, 0,
                (struct sockaddr *) dst, sizeof(struct sockaddr_in6));
   if (noc < 0) {
      perror("send_echo_request6: sendto");
      return -1;
   }
   if (noc != icmp_pkt_size) {
      fprintf(stderr, "send_echo_request6: wrote %d bytes, ret=%d\n",
              icmp_pkt_size, noc);
      return -1;
   }
   return 0;
}

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 See ) 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 See à See ) 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 See ), 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é.


#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip6.h>
#include <netinet/icmp6.h>
#include <arpa/inet.h>
#include <errno.h>
#include <signal.h>
#include <setjmp.h>
 
#ifndef MAX_DATALEN
#define MAX_DATALEN (1280 - sizeof(struct ip6_hdr) - sizeof(struct icmp6_hdr))
#endif
 
static void on_timeout(int);
static int recv_icmp_pkt(int, struct sockaddr_in6 *, uint16_t, uint16_t);
 
static u_char buf[sizeof(struct icmp6_hdr) + MAX_DATALEN];
static jmp_buf j_buf;
 
void wait_for_echo_reply6(int sock, struct sockaddr_in6 *from, uint16_t id,
                          uint16_t seq, int timeout)
{
struct icmp6_filter filter;
char from_ascii[INET6_ADDRSTRLEN];
 
   inet_ntop(AF_INET6, &from->sin6_addr, from_ascii, INET6_ADDRSTRLEN);
 
   ICMP6_FILTER_SETBLOCKALL(&filter);
   ICMP6_FILTER_SETPASS(ICMP6_ECHO_REPLY, &filter);
   setsockopt(sock, IPPROTO_ICMPV6, ICMP6_FILTER, (const void *) &filter,
              sizeof(filter));
   signal(SIGALRM, on_timeout);
   alarm(timeout);
   for (;;) {
      int noc, from_len = sizeof(struct sockaddr_in6);
 
      if (setjmp(j_buf) == SIGALRM) {
         fprintf(stderr, "No answer from %s\n", from_ascii);
         break;
      }
      noc = recvfrom(sock, buf, sizeof(buf), 0,
                     (struct sockaddr *) from, &from_len);
      if (noc < 0) {
         if (errno == EINTR)
            continue;
         perror("wait_for_echo_reply6: recvfrom");
         continue;
      }
      if (recv_icmp_pkt(noc, from, id, seq) == 0)
         break;
   }
   alarm(0);
   signal(SIGALRM, SIG_DFL);
   return;
}
 
static void on_timeout(int sig)
{
   longjmp(j_buf, sig);
}

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 See et See ). 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.


static int recv_icmp_pkt(int noc, struct sockaddr_in6 *from, uint16_t id,
                         uint16_t seq)
{
int opt_data_size;
char from_ascii[INET6_ADDRSTRLEN];
struct icmp6_hdr *icmp;
 
   if (inet_ntop(AF_INET6, &from->sin6_addr, from_ascii,
                 INET6_ADDRSTRLEN) == NULL) {
      perror("inet_ntop");
      return -1;
   }
   if (noc < sizeof(struct icmp6_hdr)) {
      fprintf(stderr, "recv_icmp_pkt: packet too short from %s\n",
              from_ascii);
      return -1;
   }
   opt_data_size = noc - sizeof(struct icmp6_hdr);
   icmp = (struct icmp6_hdr *) buf;
   if (icmp->icmp6_id != id || icmp->icmp6_seq != seq)
      return 1;
   fprintf(stdout, "Got answer from %s (seq = %d)\n", from_ascii, seq);
   if (opt_data_size > 0) {
      fprintf(stdout, "with data [\n");
      fflush(stdout);
      if (opt_data_size > MAX_DATALEN) {
         fprintf(stderr,
                 "recv_icmp_pkt: received too much data from %s\n",
                 from_ascii);
      }
      else
         write(1, (char *) icmp + sizeof(struct icmp6_hdr), opt_data_size);
      fprintf(stdout, "\n] (end of data)\n");
   }
   return 0;
}

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.


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/icmp6.h>
#include <arpa/inet.h>
#include <netdb.h>
#ifdef __linux__
#include <linux/version.h>
#if LINUX_VERSION_CODE < KERNEL_VERSION(2,4,19)
#define LINUX_CKSUM_CALCUL_EXPLICITE
#endif
#endif
 
#ifndef TIMEOUT
#define TIMEOUT 5
#endif
 
extern int send_echo_request6(int, struct sockaddr_in6 *, uint16_t,
uint16_t, char *, int);
extern void wait_for_echo_reply6(int, struct sockaddr_in6 *, uint16_t,
uint16_t, int);
 
static void usage(char *);
 
int main(int argc, char **argv)
{
int sock, timeout = TIMEOUT, a, ecode;
char *opt_data = NULL, *dst_ascii;
int opt_data_size = 0;
uint16_t id, seq = 0;
struct sockaddr_in6 *dst;
struct addrinfo *res;
struct addrinfo hints = {
AI_CANONNAME,
PF_INET6,
SOCK_RAW,
IPPROTO_ICMPV6,
0,
NULL,
NULL,
NULL
};
 
while((a = getopt(argc, argv, "d:s:t:")) != EOF)
switch(a) {
case 'd':
opt_data = optarg;
opt_data_size = strlen(optarg) + 1;
break;
case 's':
seq = (uint16_t) atoi(optarg);
break;
case 't':
timeout = atoi(optarg);
break;
default:
usage(*argv);
}
argc -= optind;
if (argc != 1)
usage(*argv);
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 See ) : 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 See à See ) 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 See , 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).


ecode = getaddrinfo(*argv, NULL, &hints, &res);
if (ecode) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ecode));
exit(1);
}
dst_ascii = res->ai_canonname ? res->ai_canonname : *argv;
dst = (struct sockaddr_in6 *) res->ai_addr;
if (IN6_IS_ADDR_MULTICAST(&dst->sin6_addr)) {
fprintf(stderr, "%s multicast address not supported\n", dst_ascii);
exit(1);
}
if ((sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol)) < 0) {
perror("socket (RAW)");
exit(1);
}
#ifdef LINUX_CKSUM_CALCUL_EXPLICITE
{
/*
* Pour linux avant 2.4.19, il faut demander le calcul des checksums
* sur les sockets raw, meme pour des paquets icmpv6
*/
#define OFFSETOF(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
int off = OFFSETOF(struct icmp6_hdr, icmp6_cksum);
 
if (setsockopt(sock, SOL_RAW, IPV6_CHECKSUM, &off, sizeof off) < 0) {
perror("setsockopt (IPV6_CHECKSUM)");
exit(1);
}
}
#endif
id = (uint16_t) (getpid() & 0xffff);
fprintf(stdout, "Sending ECHO REQUEST to: %s\n", dst_ascii);
if (send_echo_request6(sock, dst, id, seq, opt_data,
opt_data_size) < 0)
exit(1);
fprintf(stdout, "Waiting for answer (timeout = %ds)...\n", timeout);
wait_for_echo_reply6(sock, dst, id, seq, timeout);
close(sock);
exit(0);
}
 
static void usage(char *s)
{
fprintf(stderr, "Usage: %s [-d data] [-s seq] [-t timeout] host | addr\n", s);
exit(1);
}
Personal tools