Книга: UNIX: разработка сетевых приложений

28.5. Программа ping

28.5. Программа ping

В данном разделе приводится версия программы ping, работающая как с IPv4, так и с IPv6. Вместо того чтобы представить известный доступный исходный код, мы разработали оригинальную программу, и сделано это по двум причинам. Во-первых, свободно доступная программа ping страдает общей болезнью программирования, известной как «ползучий улучшизм» (стремление к постоянным ненужным усложнениям программы в погоне за мелкими улучшениями): она поддерживает 12 различных параметров. Наша цель при исследовании программы ping в том, чтобы понять концепции и методы сетевого программирования и не быть при этом сбитыми с толку ее многочисленными параметрами. Наша версия программы ping поддерживает только один параметр и занимает в пять раз меньше места, чем общедоступная версия. Во-вторых, общедоступная версия работает только с IPv4, а нам хочется показать версию, поддерживающую также и IPv6.

Действие программы ping предельно просто: по некоторому IP-адресу посылается эхо-запрос ICMP, и этот узел отвечает эхо-ответом ICMP. Оба эти сообщения поддерживаются в обеих версиях — и в IPv4, и в IPv6. На рис. 28.1 приведен формат ICMP-сообщений.


Рис. 28.1. Формат сообщений эхо-запроса и эхо-ответа ICMPv4 и ICMPv6

В табл. А.5 и А.6 приведены значения поля тип (type) для этих сообщений и говорится, что значение поля код (code) равно нулю. Далее будет показано, что в поле идентификатор (identifier) указывается идентификатор процесса ping, а значение поля порядковый номер (sequence number) увеличивается на 1 для каждого отправляемого пакета. В поле дополнительные данные (optional data) сохраняется 8-байтовая отметка времени отправки пакета. Правила ICMP-запроса требуют, чтобы идентификатор, порядковый номер и все дополнительные данные возвращались в эхо-ответе. Сохранение отметки времени отправки пакета позволяет вычислить RTT при получении ответа.

В листинге 28.1[1] приведены примеры работы нашей программы. В первом используется версия IPv4, а во втором IPv6. Обратите внимание, что мы установили для нашей программы ping флаг set-user-ID (установка идентификатора пользователя при выполнении), потому что для создания символьного сокета требуются права привилегированного пользователя.

Листинг 28.1. Примеры вывода программы ping

freebsd % ping www.google.com
PING www.google.com (216.239.57.99): 56 data bytes
64 bytes from 216.239.57.99: seq=0, ttl=53, rtt=5.611 ms
64 bytes from 216.239.57.99: seq=1, ttl=53, rtt=5.562 ms
64 bytes from 216.239.57 99: seq=2, ttl=53, rtt=5.589 ms
64 bytes from 216.239.57.99: seq=3, ttl=53, rtt=5.910 ms
freebsd % ping www.kame.net
PING orange.kame.net (2001:200:0:4819:203:47ff:fea5:3085): 56 data bytes
64 bytes from 2001:200:0:4819:203:47ff:fea5:3085: seq=0, hlim=52, rtt=422.066 ms
64 bytes from 2001:200:0:4819:203:47ff:fea5:3085: seq=1, hlim=52, rtt=417.398 ms
64 bytes from 2001:200:0:4819:203:47ff:fea5:3085: seq=2, hlim=52, rtt=416.528 ms
64 bytes from 2001:200:0:4819.203.47ff:fea5:3085: seq=3, hlim=52, rtt=429.192 ms

На рис. 28.2 приведен обзор функций, составляющих программу ping.


Рис. 28.2. Обзор функций программы ping

Данная программа состоит из двух частей: одна половина читает все, что приходит на символьный сокет, и выводит эхо-ответы ICMP, а другая половина один раз в секунду посылает эхо-запросы ICMP. Вторая половина запускается один раз в секунду сигналом SIGALRM.

В листинге 28.2 приведен заголовочный файл ping.h, подключаемый во всех файлах программы.

Листинг 28.2. Заголовочный файл ping.h

//ping/ping.h
 1 #include "unp.h"
 2 #include <netinet/in_systm.h>
 3 #include <netinet/in.h>
 4 #include <netinet/ip_icmp.h>
 5 #define BUFSIZE 1500
 6 /* глобальные переменные */
 7 char sendbuf[BUFSIZE];
 8 int datalen; /* размер данных после заголовка ICMP */
 9 char *host;
10 int nsent; /* увеличиваем на 1 для каждого sendto() */
11 pid_t pid; /* наш PID */
12 int sockfd;
13 int verbose;
14 /* прототипы функций */
15 void init_v6(void);
16 void proc_v4(char*, ssize_t, struct msghdr*, struct timeval*);
17 void proc_v6(char*, ssize_t., struct msghdr*, struct timeval*);
18 void send_v4(void);
19 void send_v6(void):
20 void readloop(void);
21 void sig_alrm(int);
22 void tv_sub(struct timeval*, struct timeval*);
23 struct proto {
24  void (*fproc)(char*, ssize_t, struct msghdr*, struct timeval*);
25  void (*fsend)(void);
26  void (*finit)(void);
27  struct sockaddr *sasend; /* структура sockaddr{} для отправки,
                                полученная от getaddrinfo */
28  struct sockaddr *sarecv; /* sockaddr{} для получения */
29  socklen_t salen; /* длина sockaddr{} */
30  int icmpproto; /* значение IPPROTO_xxx для ICMP */
31 } *pr;
32 #ifdef IPV6
33 #include <netinet/ip6.h>
34 #include <netinet/icmp6.h>
35 #endif

Подключение заголовочных файлов IPv4 и ICMPv4

1-22 Подключаются основные заголовочные файлы IPv4 и ICMPv4, определяются некоторые глобальные переменные и прототипы функций.

Определение структуры proto

23-31 Для обработки различий между IPv4 и IPv6 используется структура proto. Данная структура содержит два указателя на функции, два указателя на структуры адреса сокета, размер структуры адреса сокета и значение протокола для ICMP. Глобальный указатель pr будет указывать на одну из этих структур, которая будет инициализироваться для IPv4 или IPv6.

Подключение заголовочных файлов IPv6 и ICMPv6

32-35 Подключаются два заголовочных файла, определяющие структуры и константы IPv6 и ICMPv6 (RFC 3542 [114]).

Функция main приведена в листинге 28.3.

Листинг 28.3. Функция main

//ping/main.c
 1 #include "ping.h"
 2 struct proto proto_v4 =
 3 { proc_v4, send_v4, NULL, NULL, NULL, 0, IPPROTO_ICMP };
 4 #ifdef IPV6
 5 struct proto proto_v6 =
 6 { proc_v6, send_v6, init_v6, NULL, NULL, 0, IPPROTO_ICMPV6 };
 7 #endif
 8 int datalen = 56; /* размер данных в эхо-запросе ICMP */
 9 int
10 main(int argc, char **argv)
11 {
12  int c;
13  struct addrinfo *ai;
14  char *h;
15  opterr = 0; /* отключаем запись сообщений getopt() в stderr */
16  while ((с = getopt(argc, argv, "v")) != -1) {
17   switch (c) {
18   case 'v':
19    verbose++;
20    break;
21   case '?':
22    err_quit("unrecognized option %c", c);
23   }
24  }
25  if (optind != argc-1)
26   err_quit("usage: ping [ -v ] <hostname>");
27  host = argv[optind];
28  pid = getpid() & 0xffff; /* поле идентификатора ICMP имеет размер 16 бит */
29  Signal(SIGALRM, sig_alrm);
30  ai = Host_serv(host, NULL, 0, 0);
31  h = Sock_ntop_host(ai->ai_addr, ai->ai_addrlen);
32  printf("PING %s (%s): %d data bytesn",
33  ai->ai_canonname ? ai->ai_canonname : h, h, datalen);
34  /* инициализация в соответствии с протоколом */
35  if (ai->ai_family == AF_INET) {
36   pr = &proto_v4;
37 #ifdef IPV6
38  } else if (ai->ai_family == AF_INET6) {
39   pr = &proto_v6;
40   if (IN6_IS_ADDR_V4MAPPED(&(((struct sockaddr_in6*)
41    ai->ai_addr)->sin6_addr)))
42    err_quit("cannot ping IPv4-mapped IPv6 address");
43 #endif
44  } else
45   err_quit("unknown address family %d", ai->ai_family);
46  pr->sasend = ai->ai_addr;
47  pr->sarecv = Calloc(1, ai->ai_addrlen);
48  pr->salen = ai->ai_addrlen;
49  readloop();
50  exit(0);
51 }

Определение структуры proto для IPv4 и IPv6

2-7 Определяется структура proto для IPv4 и IPv6. Указатели структуры адреса сокета инициализируются как нулевые, поскольку еще не известно, какая из версий будет использоваться — IPv4 или IPv6.

Длина дополнительных данных

8 Устанавливается количество дополнительных данных (56 байт), которые будут посылаться с эхо-запросом ICMP. При этом полная IPv4-дейтаграмма будет иметь размер 84 байта (20 байт на IPv4-заголовок и 8 байт на ICMP-заголовок), а IPv6-дейтаграмма будет иметь длину 104 байта. Все данные, посылаемые с эхо- запросом, должны быть возвращены в эхо-ответе. Время отправки эхо-запроса будет сохраняться в первых 8 байтах области данных, а затем, при получении эхо- ответа, будет использоваться для вычисления и вывода времени RTT.

Обработка параметров командной строки

15-24 Единственный параметр командной строки, поддерживаемый в нашей версии, это параметр -v, в результате использования которого большинство ICMP-сообщений будут выводиться на консоль. (Мы не выводим эхо-ответы, принадлежащие другой запущенной копии программы ping.) Для сигнала SIGALRM устанавливается обработчик, и мы увидим, что этот сигнал генерируется один раз в секунду и вызывает отправку эхо-запросов ICMP.

Обработка аргумента, содержащего имя узла

31-48 Строка, содержащая имя узла или IP-адрес, является обязательным аргументом и обрабатывается функцией host_serv. Возвращаемая структура addrinfo содержит семейство протоколов — либо AF_INET, либо AF_INET6. Глобальный указатель pr устанавливается на требуемую в конкретной ситуации структуру proto. Также с помощью вызова функции IN6_IS_ADDR_V4MAPPED мы убеждаемся, что адрес IPv6 на самом деле не является адресом IPv4, преобразованным к виду IPv6, поскольку даже если возвращаемый адрес является адресом IPv6, узлу будет отправлен пакет IPv4. (Если такая ситуация возникнет, можно переключиться и использовать IPv4.) Структура адреса сокета, уже размещенная в памяти с помощью функции getaddrinfo, используется для отправки, а другая структура адреса сокета того же размера размещается в памяти для получения.

Обработка ответов осуществляется функцией readlоор, представленной в листинге 28.4.

Листинг 28.4. Функция readloop

//ping/readlоор.c
 1 #include "ping.h"
 2 void
 3 readloop(void)
 4 {
 5  int size;
 6  char recvbuf[BUFSIZE];
 7  char controlbuf[BUFSIZE];
 8  struct msghdr msg;
 9  struct iovec iov;
10  ssize_t n;
11  struct timeval tval;
12  sockfd = Socket(pr->sasend->sa_family, SOCK_RAW, pr->icmpproto);
13  setuid(getuid()); /* права привилегированного пользователя
                         больше не нужны */
14  if (pr->finit)
15   (*pr->finit)();
16  size = 60 * 1024; /* setsockopt может завершиться с ошибкой */
17  setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));
18  sig_alrm(SIGALRM); /* отправка первого пакета */
19  iov.iov_base = recvbuf;
20  iov.iov_len = sizeof(recvbuf);
21  msg.msg_name = pr->sarecv;
22  msg.msg_iov = &iov;
23  msg.msg_iovlen = 1;
24  msg.msg_control = controlbuf;
25  for (;;) {
26   msg.msg_namelen = pr->salen;
27   msg.msg_controllen = sizeof(controlbuf);
28   n = recvmsg(sockfd, &msg, 0);
29   if (n < 0) {
30    if (errno == EINTR)
31     continue;
32    else
33     err_sys("recvmsg error");
24   }
35   Gettimeofday(&tval, NULL);
36   (*pr->fproc)(recvbuf, n, &msg, &tval);
37  }
38 }

Создание сокета

12-13 Создается символьный сокет, соответствующий выбранному протоколу. В вызове функции setuid нашему эффективному идентификатору пользователя присваивается фактический идентификатор пользователя. Для создания символьных сокетов программа должна иметь права привилегированного пользователя, но когда символьный сокет уже создан, от этих прав можно отказаться. Всегда разумнее отказаться от лишних прав, если в них нет необходимости, например на тот случай, если в программе есть скрытая ошибка, которой кто-либо может воспользоваться.

Выполнение инициализации для протокола

14-15 Мы выполняем функцию инициализации для выбранного протокола. Для IPv6 такая функция представлена в листинге 28.7.

Установка размера приемного буфера сокета

16-17 Пытаемся установить размер приемного буфера сокета, равный 61 440 байт (60?1024) — этот размер больше задаваемого по умолчанию. Это делается в расчете на случай, когда пользователь проверяет качество связи с помощью программы ping, обращаясь либо к широковещательному адресу IPv4, либо к групповому адресу. В обоих случаях может быть получено большое количество ответов. Увеличивая размер буфера, мы уменьшаем вероятность того, что приемный буфер переполнится.

Отправка первого пакета

18 Запускаем обработчик сигнала, который, как мы увидим, посылает пакет и создает сигнал SIGALRM один раз в секунду. Обычно обработчик сигналов не запускается напрямую, как у нас, но это можно делать. Обработчик сигналов является обычной функцией языка С, просто в нормальных условиях он асинхронно запускается ядром.

Подготовка msghdr для recvmsg

19-24 Мы записываем значения в неизменяемые поля структур msghdr и iovec, которые будут передаваться функции recvmsg.

Бесконечный цикл для считывания всех ICMP-сообщений

25-37 Основной цикл программы является бесконечным циклом, считывающим все пакеты, возвращаемые на символьный сокет ICMP. Вызывается функция gettimeofday для регистрации времени получения пакета, а затем вызывается соответствующая функция протокола (proc_v4 или proc_v6) для обработки ICMP-сообщения.

В листинге 28.5 приведена функция tv_sub, вычисляющая разность двух структур timeval и сохраняющая результат в первой из них.

Листинг 28.5. Функция tv_sub: вычитание двух структур timeval

//lib.tv_sub.c
 1 #include "unp.h"
 2 void
 3 tv_sub(struct timeval *out, struct timeval *in)
 4 {
 5  if ((out->tv_usec -= in->tv_usec) < 0) { /* out -= in */
 6   --out->tv_sec;
 7   out->tv_usec += 1000000;
 8  }
 9  out->tv_sec -= in->tv_sec;
10 }

В листинге 28.6 приведена функция proc_v4, обрабатывающая все принимаемые сообщения ICMPv4. Можно также обратиться к рис. А.1, на котором изображен формат заголовка IPv4. Кроме того, следует осознавать, что к тому моменту, когда процесс получает на символьном сокете ICMP-сообщение, ядро уже проверило, что основные поля в заголовке IPv4 и в сообщении ICMPv4 действительны [128, с. 214, с. 311].

Листинг 28.6. Функция proc_v4: обработка сообщений ICMPv4

//ping/prov_v4.c
 1 #include "ping.h"
 2 void
 3 proc_v4(char *ptr, ssize_t len, struct msghdr *msg, struct timeval *tvrecv)
 4 {
 5  int hlen1, icmplen;
 6  double rtt;
 7  struct ip *ip;
 8  struct icmp *icmp;
 9  struct timeval *tvsend;
10  ip = (struct ip*)ptr; /* начало IP-заголовка */
11  hlen1 = ip->ip_hl << 2; /* длина IP-заголовка */
12  if (ip->ip_p != IPPROTO_ICMP)
13   return; /* не ICMP */
14  icmp = (struct icmp*)(ptr + hlen1); /* начало ICMP-заголовка */
15  if ((icmplen = len - hlen1) < 8)
16   return; /* плохой пакет */
17  if (icmp->icmp_type == ICMP_ECHOREPLY) {
18   if (icmp->icmp_id != pid)
19    return; /* это не ответ на наш ECHO_REQUEST */
20   if (icmplen < 16)
21    return; /* недостаточно данных */
22  tvsend = (struct timeval*)icmp->icmp_data;
23  tv_sub(tvrecv, tvsend);
24  rtt = tvrecv->tv_sec * 1000.0 + tvrecv->tv_usec / 1000.0;
25  printf("%d bytes from %s: seq=%u, ttl=%d, rtt=%.3f msn",
26   icmplen, Sock_ntop_host(pr->sarecv, pr->salen),
27   icmp->icmp_seq, ip->ip_ttl, rtt);
28  } else if (verbose) {
29   printf(" %d bytes from %s: type = %d, code = %dn",
30   icmplen, Sock_ntop_host(pr->sarecv, pr->salen),
31   icmp->icmp_type, icmp->icmp_code);
32  }
33 }

Извлечение указателя на ICMP-заголовок

10-16 Значение поля длины заголовка IPv4, умноженное на 4, дает размер заголовка IPv4 в байтах. (Следует помнить, что IPv4-заголовок может содержать параметры.) Это позволяет нам установить указатель icmp так, чтобы он указывал на начало ICMP-заголовка. Мы проверяем, относится ли данный пакет к протоколу ICMP и имеется ли в нем достаточно данных для проверки временной отметки, включенной нами в эхо-запрос. На рис. 28.3 приведены различные заголовки, указатели и длины, используемые в коде.


Рис. 28.3. Заголовки, указатели и длина при обработке ответов ICMPv4

Проверка эхо-ответа ICMP

17-21 Если сообщение является эхо-ответом ICMP, то необходимо проверить поле идентификатора, чтобы выяснить, относится ли этот ответ к посланному данным процессом запросу. Если программа ping запущена на одном узле несколько раз, каждый процесс получает копии всех полученных ICMP-сообщений.

22-27 Путем вычитания времени отправки сообщения (содержащегося в части ICMP-ответа, отведенной под дополнительные данные) из текущего времени (на которое указывает аргумент функции tvrecv) вычисляется значение RTT. Время RTT преобразуется из микросекунд в миллисекунды и выводится на экран вместе с полем порядкового номера и полученным значением TTL. Поле порядкового номера позволяет пользователю проследить, не были ли пакеты пропущены, переупорядочены или дублированы, а значение TTL показывает количество транзитных узлов между двумя узлами.

Вывод всех полученных ICMP-сообщений при включении параметра verbose

28-32 Если пользователем указан параметр командной строки -v, также выводятся поля типа и кода из всех других полученных ICMP-сообщений.

Обработка сообщений ICMPv6 управляется функцией proc_v6, приведенной в листинге 28.8. Она аналогична функции proc_v4, представленной в листинге 28.6. Однако поскольку символьные сокеты IPv6 не передают процессу заголовок IPv6, ограничение на количество транзитных узлов приходится получать в виде вспомогательных данных. Для этого нам приходится подготавливать сокет функцией init_v6, представленной в листинге 28.7.

Листинг 28.7. Функция init_v6: подготовка сокета

 1 void
 2 init_v6()
 3 {
 4 #ifdef IPV6
 5  int on = 1;
 6  if (verbose == 0) {
 7   /* установка фильтра, пропускающего только пакеты ICMP6_ECHO_REPLY. если
        не включен параметр verbose (вывод всех ICMP-сообщений) */
 8   struct icmp6_filter myfilt;
 9   ICMP6_FILTER_SETBLOCKALL(&myfilt);
10   ICMP6_FILTER_SETPASS(ICMP6_ECHO_REPLY, &myfilt);
11   setsockopt(sockfd, IPPROTO_IPV6, ICMP6_FILTER, &myfilt,
12    sizeof(myfilt));
13   /* игнорируем ошибку, потому что фильтр - необязательная оптимизация */
14  }
15  /* следующую ошибку тоже игнорируем; придется обойтись без вывода
       ограничения на количество транзитных узлов */
16 #ifdef IPV6_RECVHOPLIMIT
17  /* RFC 3542 */
18  setsockopt(sockfd, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, &on, sizeof(on));
19 #else
20  /* RFC 2292 */
21  setsockopt(sockfd, IPPROTO_IPV6, IPV6_HOPLIMIT, &on, sizeof(on));
22 #endif
23 #endif
24 }

Приведенная в листинге 28.8 функция proc_v6 обрабатывает входящие пакеты.

Листинг 28.8. Функция proc_v6: обработка сообщений ICMPv6

//ping/proc_v6.c
 1 #include "ping.h"
 2 void
 3 proc_v6(char *ptr, ssize_t len, struct msghdr *msg, struct timeval* tvrecv)
 4 {
 5 #ifdef IPV6
 6  double rtt;
 7  struct icmp6_hdr *icmp6;
 8  struct timeval *tvsend;
 9  struct cmsghdr *cmsg;
10  int hlim;
11  icmp6 = (struct icmp6_hdr*)ptr;
12  if (len < 8)
13   return; /* плохой пакет */
14  if (icmp6->icmp6_type == ICMP6_ECHO_REPLY) {
15   if (icmp6->icmp6_id != pid)
16    return; /* это не ответ на наш ECHO_REQUEST */
17   if (len < 16)
18    return; /* недостаточно данных */
19   tvsend = (struct timeval*)(icmp6 + 1);
20   tv_sub(tvrecv, tvsend);
21   rtt = tvrecv->tv_sec * 1000.0 + tvrecv->tv_usec / 1000.0;
22   hlim = -1;
23   for (cmsg = CMSG_FIRSTHDR(msg); cmsg != NULL;
24    cmsg = CMSG_NXTHDR(msg, cmsg)) {
25    if (cmsg->cmsg_level == IPPROTO_IPV6 &&
26     cmsg->cmsg_type == IPV6_HOPLIMIT) {
27     hlim = *(u_int32_t*)CMSG_DATA(cmsg);
28     break;
29    }
30   }
31   printf("%d bytes from %s; seq=%u, hlim=",
32    len, Sock_ntop__host(pr->sarecv, pr->salen), icmp6->icmp6_seq);
33   if (hlim == -1)
34    printf("???"); /* отсутствуют вспомогательные данные */
35   else
36    printf("%d", hlim);
37   printf(", rtt=%.3f msn", rtt);
38  } else if (verbose) {
39   printf(" %d bytes from type = %d, code = %dn",
40    len, Sock_ntop_host(pr->sarecv, pr->salen);
41   icmp6->icmp6, type, icmp6->icmp6_code);
42  }
43 #endif /* IPV6 */
44 }

Извлечение указателя на заголовок ICMPv6

11-13 Заголовок ICMPv6 возвращается внутри данных при чтении из сокета. (Напомним, что дополнительные заголовки IPv6, если они присутствуют, всегда возвращаются не как стандартные данные, а как вспомогательные.) На рис. 28.4 приведены различные заголовки, указатели и длина, используемые в коде.


Рис. 28.4. Заголовки, указатели и длина при обработке ответов ICMPv6

Проверка эхо-ответа ICMP

14-37 Если ICMP-сообщение является эхо-ответом, то чтобы убедиться, что ответ предназначен для нас, мы проверяем поле идентификатора. Если это подтверждается, то вычисляется значение RTT, которое затем выводится вместе с порядковым номером и предельным количеством транзитных узлов IPv4. Ограничение на количество транзитных узлов мы получаем из вспомогательных данных IPV6_HOPLIMIT.

Вывод всех полученных ICMP-сообщений при включении параметра verbose

38-42 Если пользователь указал параметр командной строки -v, выводятся также поля типа и кода всех остальных получаемых ICMP-сообщений.

Обработчиком сигнала SIGALRM является функция sig_alrm, приведенная в листинге 28.9. В листинге 28.4 функция readloop вызывает обработчик сигнала один раз для отправки первого пакета. Эта функция в зависимости от протокола вызывает функцию send_v4 или send_v6 для отправки эхо-запроса ICMP и далее программирует запуск другого сигнала SIGALRM через 1 с.

Листинг 28.9. Функция sig_alrm: обработчик сигнала SIGALRM

//ping/sig_alrm.c
 1 #include "ping.h"
 2 void
 3 sig_alrm(int signo)
 4 {
 5  (*pr->fsend)();
 6  alarm(1);
 7  return;
 8 }

Функция send_v4, приведенная в листинге 28.10, строит ICMPv4 сообщение эхо-запроса и записывает его в символьный сокет.

Листинг 28.10. Функция send_v4: построение эхо-запроса ICMPv4 и его отправка

//ping/send_v4.c
 1 #include "ping.h"
 2 void
 3 send_v4(void)
 4 {
 5  int len;
 6  struct icmp *icmp;
 7  icmp = (struct icmp*)sendbuf;
 8  icmp->icmp_type = ICMP_ECHO;
 9  icmp->icmp_code = 0;
10  icmp->icmp_id = pid;
11  icmp->icmp_seq = nsent++;
12  memset(icmp->icmp_data, 0xa5, datalen); /* заполнение по шаблону */
13  Gettimeofday((struct timeval*)icmp->icmp_data, NULL);
14  len = 8 + datalen; /* контрольная сумма по заголовку и данным */
15  icmp->icmp_cksum = 0;
16  icmp->icmp_cksum = in_cksum((u_short*)icmp, len);
17  Sendto(sockfd, sendbuf, len, 0, pr->sasend, pr->salen);
18 }

Формирование ICMP-сообщения

7-13 ICMPv4 сообщение сформировано. В поле идентификатора установлен идентификатор нашего процесса, а порядковый номер установлен как глобальная переменная nset, которая затем увеличивается на 1 для следующего пакета. Текущее время сохраняется в части данных ICMP-сообщения.

Вычисление контрольной суммы ICMP

14-16 Для вычисления контрольной суммы ICMP значение поля контрольной суммы устанавливается равным 0, затем вызывается функция in_cksum, а результат сохраняется в поле контрольной суммы. Контрольная сумма ICMPv4 вычисляется по ICMPv4-заголовку и всем следующим за ним данным.

Отправка дейтаграммы

17 ICMP-сообщение отправлено на символьный сокет. Поскольку параметр сокета IP_HDRINCL не установлен, ядро составляет заголовок IPv4 и добавляет его в начало нашего буфера.

Контрольная сумма Интернета является суммой обратных кодов 16-разрядных значений. Если длина данных является нечетным числом, то для вычисления контрольной суммы к данным дописывается один нулевой байт. Перед вычислением контрольной суммы поле контрольной суммы должно быть установлено в 0. Такой алгоритм применяется для вычисления контрольных сумм IPv4, ICMPv4, IGMPv4, ICMPv6, UDP и TCP. В RFC 1071 [12] содержится дополнительная информация и несколько числовых примеров. В разделе 8.7 книги [128] более подробно рассказывается об этом алгоритме, а также приводится более эффективная его реализация. В нашем случае контрольную сумму вычисляет функция in_cksum, приведенная в листинге 28.11.

Листинг 28.11. Функция in_cksum: вычисление контрольной суммы Интернета

//libfree/in_cksum.c
 1 uint16_t
 2 in_cksum(uint16_t *addr, int len)
 3 {
 4  int nleft = len;
 5  uint32_t sum = 0;
 6  uint16_t *w = addr;
 7  uint16_t answer = 0;
 8  /*
 9   * Наш алгоритм прост: к 32-разрядному аккумулятору sum мы добавляем
10   * 16-разрядные слова, а затем записываем все биты переноса из старших
11   * 16 разрядов в младшие 16 разрядов.
12   */
13  while (nleft > 1) {
14   sum += *w++;
15   nleft -= 2;
16  }
17  /* при необходимости добавляем четный байт */
18  if (nleft == 1) {
19   *(unsigned char*)(&answer) = *(unsigned char*)w;
20   sum += answer;
21  }
22  /* перемещение битов переноса из старших 16 разрядов в младшие */
23  sum = (sum >> 16) + (sum & 0xffff); /* добавление старших 16 к младшим */
24  sum += (sum >> 16); /* добавление переноса */
25  answer = ~sum; /* обрезаем по 16 разрядам */
26  return(answer);
27 }

Алгоритм вычисления контрольной суммы Интернета

1-27 Первый цикл while вычисляет сумму всех 16-битовых значений. Если длина нечетная, то к сумме добавляется конечный байт. Алгоритм, приведенный в листинге 28.11, является простым алгоритмом, подходящим для программы ping, но неудовлетворительным для больших объемов вычислений контрольных сумм, производимых ядром.

ПРИМЕЧАНИЕ

Эта функция взята из общедоступной версии программы ping, написанной Майком Мюссом (Mike Muuss).

Последней функцией нашей программы ping является функция send_v6, приведенная в листинге 28.12, которая формирует и посылает эхо-запросы ICMPv6.

Функция send_v6 аналогична функции send_v4, но обратите внимание, что она не вычисляет контрольную сумму. Как отмечалось ранее, поскольку для вычисления контрольной суммы ICMPv6 используется адрес отправителя из IPv6-заголовка, данная контрольная сумма вычисляется для нас ядром, после того как ядро выяснит адрес отправителя.

Листинг 28.12. Функция send_v6: построение и отправка ICMPv6-сообщения эхо-запроса

//ping/send_v6.c
 1 #include "ping.h"
 2 void
 3 send_v6()
 4 {
 5 #ifdef IPV6
 6  int len;
 7  struct icmp6_hdr *icmp6;
 8  icmp6 = (struct icmp6_hdr*)sendbuf,
 9  icmp6->icmp6_type = ICMP6_ECHO_REQUEST;
10  icmp6->icmp6_code = 0;
11  icmp6->icmp6_id = pid;
12  icmp6->icmp6_seq = nsent++;
13  memset((icmp6 + 1), 0xa5, datalen); /* заполнение по шаблону */
14  Gettimeofday((struct timeval*)(icmp6 + 1), NULL);
15  len = 8 + datalen; /* 8-байтовый заголовок ICMPv6 */
16  Sendto(sockfd, sendbuf, len, 0, pr->sasend, pr->salen);
17  /* ядро вычисляет и сохраняет контрольную сумму само */
18 #endif /* IPV6 */
19 }

Оглавление книги


Генерация: 0.104. Запросов К БД/Cache: 0 / 0
поделиться
Вверх Вниз