Теперь мы приступаем к рассмотрению примера, в котором отсылается дейтаграмма UDP, содержащая запрос UDP к серверу имен, а затем считывается ответ с помощью библиотеки захвата пакетов. Цель данного примера — установить, вычисляется на сервере имен контрольная сумма UDP или нет. В случае IPv4 вычисление контрольной суммы не является обязательным. В большинстве систем в настоящее время вычисление контрольных сумм по умолчанию включено, но, к сожалению, в более старых системах, в частности SunOS 4.1.x, оно по умолчанию отключено. В настоящее время все системы, а особенно система, в которой работает сервер имен, всегда должны работать с включенными контрольными суммами UDP, поскольку поврежденные (содержащие ошибки) дейтаграммы могут повредить базу данных сервера.
ПРИМЕЧАНИЕ
Включение и выключение контрольных сумм обычно осуществляется сразу для всей системы, как показано в приложении Е [111].
Мы формируем дейтаграмму UDP (запрос DNS) и записываем ее в символьный сокет. Параллельно мы проделаем то же самое с помощью libnet. Для отправки запроса мы могли бы использовать обычный сокет UDP, но мы хотим показать, как использовать параметр сокета IP_HDRINCL для создания полной дейтаграммы IP.
Нет возможности получить контрольную сумму UDP при чтении из обычного сокета UDP, а также считывать пакеты UDP или TCP, используя символьный сокет (см. раздел 28.4). Следовательно, путем захвата пакетов нам нужно получить целую дейтаграмму UDP, содержащую ответ сервера имен.
Затем мы исследуем поле контрольной суммы UDP в заголовке UDP, и если оно равно нулю, это означает, что на сервере отключено вычисление контрольной суммы.
Действие нашей программы иллюстрирует рис. 29.3. Мы записываем наши собственные дейтаграммы UDP в символьный сокет и считываем ответы, используя библиотеку libcap. Обратите внимание, что UDP также получает ответ сервера имен и отвечает сообщением о недоступности порта ICMP, так как ничего не знает о номере порта, выбранном нашим приложением. Сервер имен игнорирует эту ошибку ICMP. Также можно отметить, что написать подобную тестовую программу, использующую TCP, было бы сложнее, даже несмотря на то, что мы с легкостью можем записывать свои собственные сегменты TCP. Дело в том, что любой ответ на сегмент TCP, который мы генерируем, обычно инициирует отправку протоколом TCP ответного сегмента RST туда, куда был послан первый сегмент.
Рис. 29.3. Приложение, определяющее, включено ли на сервере вычисление контрольных сумм UDP
ПРИМЕЧАНИЕ
Указанную проблему можно обойти. Для этого нужно посылать сегменты TCP с IP- адресом отправителя, который принадлежит присоединенной подсети, но в настоящий момент не присвоен никакому другому узлу. Нужно также добавить данные ARP на посылающем узле для этого нового IP-адреса, чтобы узел отвечал на запросы ARP для него. В результате стек IP на посылающем узле будет игнорировать пакеты, приходящие на этот IP-адрес, в предположении, что посылающий узел не является маршрутизатором.
На рис. 29.4 приведены функции, используемые в нашей программе.
Рис. 29.4. Функции, которые используются в программе udpcksum
В листинге 29.1 показан заголовочный файл udpcksum.h, в который включен наш базовый заголовочный файл unp.h, а также различные системные заголовки, необходимые для получения доступа к определениям структур для заголовков пакетов IP и UDP.
Листинг 29.1. Заголовочный файл udpcksum.h
//udpcksum/udpcksum.h
1 #include "unp.h"
2 #include
3 #include /* необходим для ip.h */
4 #include
5 #include
6 #include
7 #include
8 #include
9 #include
10 #include
11 #define TTL_OUT 64 /* исходящее TTL */
12 /* объявление глобальных переменных */
13 extern struct sockaddr *dest, *local;
14 extern socklen_t destlen, locallen;
15 extern int datalink;
16 extern char *device;
17 extern pcap_t *pd;
18 extern int rawfd;
19 extern int snaplen;
20 extern int verbose;
21 extern int zerosum;
22 /* прототипы функций */
23 void cleanup(int);
24 char *next_pcap(int*);
25 void open_output(void);
26 void open_pcap(void);
27 void send_dns_query(void);
28 void test_udp(void);
29 void udp_write(char*, int);
30 struct udpiphdr *udp_read(void);
3-10 Для работы с полями заголовков IP и UDP требуются дополнительные заголовочные файлы Интернета.
11-30 Мы определяем некоторые глобальные переменные и прототипы для своих собственных функций, которые вскоре покажем.
Первая часть функции main показана в листинге 29.2.
Листинг 29.2. Функция main: определения
//udpcksum/main.c
1 #include "udpcksum.h"
2 /* определение глобальных переменных */
3 struct sockaddr *dest, *local;
4 struct sockaddr_in locallookup;
5 socklen_t destlen, locallen;
6 int datalink; /* из pcap_datalink(), файл */
7 char *device; /* устройство pcap */
8 pcap_t *pd; /* указатель на структуру захваченных пакетов */
9 int rawfd; /* символьный сокет */
10 int snaplen = 200; /* объем захваченных данных */
11 int verbose;
12 int zerosum; /* отправка UDP-запроса без контрольной суммы */
13 static void usage(const char*);
14 int
15 main(int argc, char *argv[])
16 {
17 int c, lopt=0;
18 char *ptr, localname[1024], *localport;
19 struct addrinfo *aip;
В следующей части функции main, представленной в листинге 29.3, обрабатываются аргументы командной строки.
Листинг 29.3. Функция main: обработка аргументов командной строки
//udpcksum/main.c
20 opterr = 0; /* отключаем запись сообщений getopt() в stderr */
21 while ((с = getopt(argc, argv, "0i:l:v")) != -1) {
22 switch (с) {
23 case '0':
24 zerosum = 1;
25 break;
26 case 'i';
27 device = optarg; /* устройство pcap */
28 break;
29 case 'l'; /* локальный IP адрес и номер порта; a.b.c.d.p */
30 if ((ptr = strrchr(optarg, '.')) == NULL)
31 usage("invalid -l option");
32 *ptr++ = 0; /* нуль заменяет последнюю точку. */
33 local port = ptr; /* имя сервиса или номер порта */
34 strncpy(localname, optarg, sizeof(localname));
35 lopt = 1;
36 break;
37 case 'v':
38 verbose = 1;
39 break;
40 case '?':
41 usage("unrecognized option");
42 }
43 }
Обработка аргументов командной строки
20-25 Мы вызываем функцию getopt для обработки аргументов командной строки. С помощью параметра -0 мы посылаем запросы UDP без контрольной суммы UDP, чтобы выяснить, обрабатываются ли эти дейтаграммы сервером иначе, чем дейтаграммы с контрольной суммой.
26 Параметр -i позволяет нам задать интерфейс, на котором будут приниматься ответы сервера. Если этот интерфейс не будет задан, библиотека для захвата пакетов выберет какой-либо интерфейс самостоятельно, но в случае узла с несколькими сетевыми интерфейсами этот выбор может оказаться некорректным. В этом заключается одно из различий между считыванием из обычного сокета и из устройства для захвата пакетов: в первом случае мы можем указать универсальный локальный адрес, что позволяет получать пакеты, прибывающие на любой из сетевых интерфейсов. Но во втором случае при работе с устройством для захвата пакетов мы можем получать пакеты, прибывающие только на конкретный интерфейс.
ПРИМЕЧАНИЕ
Можно отметить, что для пакетных сокетов Linux захват пакетов не ограничен одним устройством. Тем не менее библиотека libcap обеспечивает фильтрацию либо по умолчанию, либо согласно заданному нами параметру -i.
29-36 Параметр -l позволяет нам задать IP-адрес отправителя и номер порта. В качестве номера порта (или названия службы) берется строка, следующая за последней точкой, а IP-адресом является все, что расположено перед последней точкой.
Последняя часть функции main показана в листинге 29.4.
Листинг 29.4. Функция main: преобразование имен узлов и названий служб, создание сокета
//udpcksum/main.c
44 if (optind != argc-2)
45 usage("missing and/or ");
46 /* преобразование имени получателя и службы */
47 aip = Host_serv(argv[optind], argv[optind+1], AF_INET, SOCK_DGRAM);
48 dest = aip->ai_addr; /* не освобождаем память при помощи freeaddrinfo() */
49 destlen = aip->ai_addrlen;
50 /*
51 * Нужен локальный IP-адрес для указания в UDP-дейтаграммах.
52 * Нельзя задать 0 и предоставить выбор уровню IP,
53 * потому что адрес нужен для вычисления контрольной суммы.
54 * Если указан параметр -1, используем заданные при вызове значения.
55 * в противном случае соединяем сокет UDP с адресатом и определяем
56 * правильный адрес отправителя.
57 */
58 if (lopt) {
59 /* преобразование локального имени и сервиса */
60 aip = Host_serv(localname, localport, AF_INET, SOCK_DGRAM);
61 local = aip->ai_addr; /* не вызываем freeaddrinfo() */
62 locallen = aip->ai_addrlen;
63 } else {
64 int s;
65 s = Socket(AF_INET, SOCK_DGRAM, 0);
66 Connect(s, dest, destlen);
67 /* ядро выбирает правильный локальный адрес */
68 locallen = sizeof(locallookup);
69 local = (struct sockaddr*)&locallookup;
70 Getsockname(s, local, &locallen);
71 if (locallookup.sin_addr.s_addr == htonl(INADDR_ANY))
72 err_quit("Can't determine local address - use -l\n");
73 close(s);
74 }
75 open_output(); /* открываем поток вывода (символьный сокет или libnet) */
76 open_pcap(); /* открываем устройство захвата пакетов */
77 setuid(getuid()); /* права привилегированного пользователя больше
не нужны */
78 Signal(SIGTERM, cleanup);
79 Signal(SIGINT, cleanup);
80 Signal(SIGHUP, cleanup);
81 test_udp();
82 cleanup(0);
83 }
Обработка имени узла и порта получателя, затем локального имени узла и порта
46-49 Мы убеждаемся, что остается ровно два аргумента командной строки: имя узла получателя и название службы. Мы вызываем функцию host_serv для преобразования их в структуру адреса сокета, указатель на которую мы сохраняем в переменной dest.
Обработка локального имени и порта
50-74 Если в командной строке был указан соответствующий параметр, мы преобразуем имя локального узла и номер порта, сохраняя указатель на структуру адреса сокета под именем local. В противном случае для определения локального IP-адреса мы подключаемся через дейтаграммный сокет к нужному адресату и сохраняем полученный при этом локальный адрес под тем же именем local. Поскольку мы формируем собственные заголовки IP и UDP, мы должны знать IP-адрес отправителя при записи дейтаграммы UDP. Нельзя оставить адрес нулевым и предоставить уровню IP выбрать его самостоятельно, потому что адрес является частью псевдозаголовка UDP (о котором мы вскоре расскажем), используемого при вычислении контрольной суммы UDP.
Создаем символьный сокет и открываем устройство для захвата пакетов
75-76 Функция open_output выбирает метод отправки пакетов (символьный сокет или libnet). Функция open_pcap открывает устройство захвата пакетов. Она будет рассмотрена далее.
Изменение прав и установка обработчиков сигналов
77-80 Для создания символьного сокета необходимо иметь права привилегированного пользователя. Обычно такие привилегии нужны нам для того, чтобы открыть устройство для захвата пакетов, но это зависит от реализации. Например, в случае BPF администратор может установить разрешения для устройств /dev/bpf любым способом в зависимости от того, что требуется для данной системы. Здесь мы не используем эти дополнительные разрешения, предполагая, что для файла программы установлен бит SUID. Процесс выполняется с правами привилегированного пользователя, а когда они становятся не нужны, при вызове функции setuid фактический идентификатор пользователя (real user ID), эффективный идентификатор пользователя (effective user ID) и сохраненный SUID принимают значение фактического идентификатора пользователя (getuid). Мы устанавливаем обработчики сигналов на тот случай, если пользователь завершит программу раньше, чем будут изменены права.
Выполнение теста и очистка
81-82 Функция test_udp (см. листинг 29.6) выполняет тестирование и возвращает управление. Функция cleanup (см. листинг 29.14) выводит итоговую статистику библиотеки захвата пакетов, а затем завершает процесс.
В листинге 29.5 показана функция open_pcap, которую мы вызвали из функции main, чтобы открыть устройство для захвата пакетов.
Листинг 29.5. Функция open_pcap: открытие и инициализация устройства для захвата пакетов
//udpcksum/pcap.c
1 #include "udpcksum.h"
2 #define CMD "udp and src host %s and src port %d"
3 void
4 open_pcap(void)
5 {
6 uint32_t localnet, netmask;
7 char cmd[MAXLINE], errbuf[PCAP_ERRBUF_SIZE], strl[INET_ADDRSTRLEN],
8 str2[INET_ADDRSTRLEN];
9 struct bpf_program fcode;
10 if (device == NULL) {
11 if ((device = pcap_lookupdev(errbuf)) == NULL)
12 err_quit("pcap_lookup: %s", errbuf);
13 }
14 printf("device = %s\n", device);
15 /* жестко задано; promisc=0, to_ms=500 */
16 if ((pd = pcap_open_live(device, snaplen, 0, 500, errbuf)) == NULL)
17 err_quit("pcap_open_live: %s", errbuf);
18 if (pcap_lookupnet(device, &localnet, &netmask, errbuf) < 0)
19 err_quit("pcap_lookupnet %s", errbuf);
20 if (verbose)
21 printf("localnet = %s, netmask = %s\n",
22 Inet_ntop(AF_INET, &localnet, str1, sizeof(str1)),
23 Inet_ntop(AF_INET, &netmask. str2, sizeof(str2)));
24 snprintf(cmd, sizeof(cmd), CMD,
25 Sock_ntop_host(dest, destlen),
26 ntohs(sock_get_port(dest, destlen)));
27 if (verbose)
28 printf("cmd = %s\n", cmd);
29 if (pcap_compile(pd, &fcode, cmd, 0, netmask) < 0)
30 err_quit("pcap_compile: %s", pcap_geterr(pd));
31 if (pcap_setfilter(pd, &fcode) < 0)
32 err_quit("pcap_setfilter: %s", pcap_geterr(pd));
33 if ((datalink = pcap_datalink(pd)) < 0)
34 err_quit("pcap_datalink: %s", pcap_geterr(pd));
35 if (verbose)
36 printf("datalink = %d\n", datalink);
37 }
Выбор устройства для захвата пакетов
10-14 Если устройство для захвата пакетов не было задано (с помощью параметра командной строки -i), то выбор этого устройства осуществляется с помощью функции pcap_lookupdev. С помощью запроса SIOCGIFCONF функции ioctl выбирается включенное устройство с минимальным порядковым номером, но только не устройство обратной связи. Многие из библиотечных функций pcap возвращают сообщения об ошибках в виде строк. Единственным аргументом функции pcap_lookupdev является массив, в который записывается строка с сообщением об ошибке.
Открываем устройство
15-17 Функция pcap_open_live открывает устройство. Слово live присутствует в названии функции потому, что здесь имеется в виду фактическое устройство для захвата пакетов, а не файл, содержащий предыдущие сохраненные пакеты. Первым аргументом функции является имя устройства, вторым — количество байтов, которое нужно сохранять для каждого пакета (значение shaplen, которое мы инициализировали числом 200 в листинге 29.2), а третий аргумент — это флаг, указывающий на смешанный режим. Четвертый аргумент — это значение времени ожидания в миллисекундах, а пятый — указатель на массив, содержащий сообщения об ошибках.
Если установлен флаг смешанного режима, интерфейс переходит в этот режим, в результате чего он принимает все пакеты, проходящие по кабелю. Это обычное состояние программы tcpdump. Тем не менее в нашем примере ответы сервера DNS будут посланы непосредственно на наш узел (то есть можно обойтись без смешанного режима).
Четвертый аргумент — время ожидания при считывании. Вместо того чтобы возвращать пакет процессу каждый раз, когда приходит очередной пакет (что может быть весьма неэффективно, так как в этом случае потребуется выполнять множество операций копирования отдельных пакетов из ядра в процесс), это делается, когда считывающий буфер устройства оказывается заполненным либо когда истекает время ожидания. Если время ожидания при считывании равно нулю, то каждый пакет будет переправляться процессу, как только будет получен.
Получение сетевого адреса и маски подсети
18-23 Функция pcap_lookupnet возвращает сетевой адрес и маску подсети для устройства захвата пакетов. При вызове функции pcap_compile, которая будет вызвана следующей, нужно задать маску подсети, поскольку с помощью маски фильтр пакетов определяет, является ли IP-адрес адресом широковещательной передачи для данной подсети.
Компиляция фильтра пакетов
24-30 Функция pcap_compile получает строку, построенную нами как массив cmd, и компилирует ее, создавая тем самым программу для фильтрации (записывая ее в fcode). Эта программа будет отбирать те пакеты, которые мы хотим получить.
Загрузка программы фильтрации
31-32 Функция pcap_setfilter получает только что скомпилированную программу фильтрации и загружает ее в устройство для захвата пакетов. Таким образом инициируется захват пакетов, выбранных нами путем настройки фильтра.
Определение типа канального уровня
33-36 Функция pcap_datalink возвращает тип канального уровня для устройства захвата пакетов. Эта информация нужна нам при захвате пакетов для того, чтобы определить размер заголовка канального уровня, который будет добавлен в начало каждого считываемого нами пакета (см. листинг 29.10).
После вызова функции open_pcap функция main вызывает функцию test_udp, показанную в листинге 29.6. Эта функция посылает запрос DNS и считывает ответ сервера.
Листинг 29.6. Функция test_udp: отправка запросов и считывание ответов
//udpcksum/udpcksum.c
12 void
13 test_udp(void)
14 {
15 volatile int nsent = 0, timeout = 3;
16 struct udpiphdr *ui;
17 Signal(SIGALRM, sig_alrm);
18 if (sigsetjmp(jmpbuf, 1)) {
19 if (nsent >= 3)
20 err_quit("no response");
21 printf("timeout\n");
22 timeout *= 2; /* геометрическая прогрессия: 3, 6, 12 */
23 }
24 canjump = 1; /* siglongjmp разрешен */
25 send_dns_query();
26 nsent++;
27 alarm(timeout);
28 ui = udp_read();
29 canjump = 0;
30 alarm(0);
31 if (ui->ui_sum == 0)
32 printf("UDP checksums off\n");
33 else
34 printf("UDP checksums on\n");
35 if (verbose)
36 printf("received UDP checksum = %x\n", ntohs(ui->ui_sum));
37 }
Переменные volatile
15 Нам нужно, чтобы две динамические локальные переменные nsent и timeout сохраняли свои значения после возвращения siglongjmp из обработчика сигнала в нашу функцию. Реализация допускает восстановление значений динамических локальных переменных, предшествовавших вызову функции sigsetjump [110, с. 178], но добавление спецификатора volatile предотвращает это восстановление.
Установление обработчика сигналов и буфера перехода
15-16 Для сигнала SIGALRM устанавливается обработчик сигнала, а функция sigsetjmp устанавливает буфер перехода для функции siglongjmp. (Эти две функции подробно описаны в разделе 10.15 [110].) Значение 1 во втором аргументе функции sigsetjmp указывает, что требуется сохранить текущую маску сигнала, так как мы будем вызывать функцию siglongjmp из нашего обработчика сигнала.
Функция siglongjmp
19-23 Этот фрагмент кода выполняется, только когда функция siglongjmp вызывается из нашего обработчика сигнала. Вызов указывает на возникновение условий, при которых мы входим в состояние ожидания: мы отправили запрос, на который не пришло никакого ответа. Если после того, как мы отправим три запроса, ответа не будет, мы прекращаем выполнение кода. По истечении времени ожидания, отведенного на получение ответа, мы выводим соответствующее сообщение и увеличиваем значение времени ожидания в два раза, то есть задаем экспоненциальное смещение (exponential backoff), которое также описано в разделе 20.5. Первое значение времени ожидания равно 3 с, затем — 6 с и 12 с.
Причина, по которой в этом примере мы используем функции sigsetjmp и siglongjmp, вместо того чтобы просто перехватывать ошибку EINTR (как мы поступили в листинге 14.1), заключается в том, что библиотечные функции захвата пакетов (которые вызываются из нашей функции udp_read) заново запускают операцию чтения в случае возвращения ошибки EINTR. Поскольку мы не хотим модифицировать библиотечные функции, единственным решением для нас является перехватывание сигнала SIGALRM и выполнение нелокального перехода (оператора goto), который возвращает управление в наш код, а не в библиотечную функцию.
Отправка запроса DNS и считывание ответа
25-26 Функция send_dns_query (см. листинг 29.8) отправляет запрос DNS на сервер имен. Функция dns_read считывает ответ. Мы вызываем функцию alarm для предотвращения «вечной» блокировки функции read. Если истекает заданное (в секундах) время ожидания, генерируется сигнал SIGALRM, и наш обработчик сигнала вызывает функцию siglongjmp.
Анализ полученной контрольной суммы UDP
27-32 Если значение полученной контрольной суммы UDP равно нулю, это значит, что сервер не вычислил и не отправил контрольную сумму.
В листинге 29.7 показана наша функция sig_alrm — обработчик сигнала SIGALRM.
Листинг 29.7. Функция sig_alrm: обработка сигнала SIGALRM
//udpcksum/udpcksum.c
1 #include "udpcksum.h"
2 #include
3 static sigjmp_buf jmpbuf;
4 static int canjump;
5 void
6 sig_alrm(int signo)
7 {
8 if (canjump == 0)
9 return;
10 siglongjmp(jmpbuf, 1);
11 }
8-10 Флаг canjump был установлен в листинге 29.6 после инициализации буфера перехода функцией sigsetjmp. Если флаг был установлен, в результате вызова функции siglongjmp управление осуществляется таким образом, как если бы функция sigsetjmp из листинга 29.6 возвратила бы значение 1.
В листинге 29.8 показана функция send_dns_query, посылающая запрос UDP на сервер DNS. Эта функция формирует запрос DNS.
Листинг 29.8. Функция send_dns_query: отправка запроса UDP на сервер DNS
//udpcksum/senddnsquery-raw.c
6 void
7 send_dns_query(void)
8 {
9 size_t nbytes;
10 char *buf, *ptr;
11 buf = Malloc(sizeof(struct udpiphdr) + 100);
12 ptr = buf + sizeof(struct udpiphdr); /* место для заголовков IP и UDP */
13 *((uint16_t*)ptr) = htons(1234); /* идентификатор */
14 ptr += 2;
15 *((uint16_t*)ptr) = htons(0x0100); /* флаги */
16 ptr += 2;
17 *((uint16_t*)ptr) = htons(1); /* количество запросов */
18 ptr += 2;
19 *((uint16_t*)ptr) = 0; /* количество записей в ответе */
20 ptr += 2;
21 *((uint16_t*)ptr) = 0; /* количество авторитетных записей */
22 ptr += 2;
23 *((uint16_t*)ptr) = 0; /* количество дополнительных записей */
24 ptr += 2;
25 memcpy(ptr, "\001a\014root-servers\003net\000", 20);
26 ptr += 20;
27 *((uint16_t*)ptr) = htons(1); /* тип запроса = А */
28 ptr += 2;
29 *((uint16_t*)ptr) = htons(1); /* класс запроса = 1 (IP-адрес) */
30 ptr += 2;
31 nbytes = (ptr - buf) - sizeof(struct udpiphdr);
32 udp_write(buf, mbytes),
33 if (verbose)
35 printf("sent: %d bytes of data\n", nbytes);
36 }
Инициализация указателя на буфер
11-12 В буфере buf имеется место для 20-байтового заголовка IP, 8-байтового заголовка UDP и еще 100 байт для пользовательских данных. Указатель ptr установлен на первый байт пользовательских данных.
Формирование запроса DNS
13-24 Для понимания деталей устройства дейтаграммы UDP требуется понимание формата сообщения DNS. Эту информацию можно найти в разделе 14.3 [111]. Мы присваиваем полю идентификации значение 1234, сбрасываем флаги, задаем количество запросов — 1, а затем обнуляем количество записей ресурсов (RR, resource records), получаемых в ответ, количество RR, определяющих полномочия, и количество дополнительных RR.
25-30 Затем мы формируем простой запрос, который располагается после заголовка: запрос типа А IP-адреса узла a.root-servers.net. Это доменное имя занимает 20 байт и состоит из 4 фрагментов: однобайтовая часть a, 12-байтовая часть root-servers, 3-байтовая часть net и корневая часть, длина которой занимает 0 байт. Тип запроса 1 (так называемый запрос типа А), и класс запроса также 1.
Запись дейтаграммы UDP
31-32 Это сообщение состоит из 36 байт пользовательских данных (восемь 2-байтовых полей и 20-байтовое доменное имя). Мы вызываем нашу функцию udp_write для формирования заголовков UDP и IP и последующей записи дейтаграммы UDP в наш символьный сокет.
В листинге 29.9 показана функция open_output, работающая с символьными сокетами.
Листинг 29.9. Функция open_output: подготовка символьного сокета
2 int rawfd; /* символьный сокет */
3 void
4 open_output(void)
5 {
6 int on=1;
7 /*
8 * Для отправки IP-дейтаграмм нужен символьный сокет
9 * Для его создания нужны права привилегированного пользователя.
10 * Кроме того, необходимо указать параметр сокета IP_HDRINCL.
11 */
12 rawfd = Socket(dest->sa_family, SOCK_RAW, 0);
13 Setsockopt(rawfd, IPPROTO_IP, IP_HDRINCL, &on., sizeof(on));
14 }
Объявление дескриптора символьного сокета
2 Мы объявляем глобальную переменную, в которой будет храниться дескриптор символьного сокета.
Создание сокета и установка IP_HDRINCL
7-13 Мы создаем символьный сокет и включаем параметр сокета IP_HDRINCL. Это позволяет нам формировать IP-дейтаграммы целиком, включая заголовок IP.
В листинге 29.10 показана наша функция udp_write, которая формирует заголовки IP и UDP, а затем записывает дейтаграмму в символьный сокет.
Листинг 29.10. Функция udp_write: формирование заголовков UDP и IP и запись дейтаграммы IP в символьный сокет
//udpcksum/udpwrite.c
19 void
20 udp_write(char *buf, int userlen)
21 {
22 struct udpiphdr *ui;
23 struct ip *ip;
24 /* заполнение заголовка и вычисление контрольной суммы */
25 ip = (struct ip*)buf;
26 ui = (struct udpiphdr*)buf;
27 bzero(ui, sizeof(*ui));
28 /* добавляем 8 к длине псевдозаголовка */
29 ui->ui_len = htons((uint16_t)(sizeof(struct udphdr) + userlen));
30 /* добавление 28 к длине IP-дейтаграммы */
31 userlen += sizeof(struct udpiphdr);
32 ui->ui_pr = IPPROTO_UDP;
33 ui->ui_src.s_addr = ((struct sockaddr_in*)local)->sin_addr.s_addr;
34 ui->ui_dst.s_addr = ((struct sockaddr_in*)dest)->sin_addr.s_addr;
35 ui->ui_sport = ((struct sockaddr_in*)local)->sin_port;
36 ui->ui_dport = ((struct sockaddr_in*)dest)->sin_port;
37 ui->ui_ulen = ui->ui_len;
38 if (zerosum == 0) {
39 #if 1 /* заменить на if 0 для Solaris 2.x. x < 6 */
40 if ((ui->ui_sum = m_cksum((u_int16_t*)in, userlen)) == 0)
41 ui->ui_sum = 0xffff;
42 #else
43 ui->ui_sum = ui->ui_len;
44 #endif
45 }
46 /* заполнение оставшейся части IP-заголовка */
47 /* функция p_output() вычисляет и сохраняет контрольную сумму IP */
48 ip->ip_v = IPVERSION;
49 ip->ip_hl = sizeof(struct ip) >> 2;
50 ip->ip_tos = 0;
51 #if defined(linux) || defined(__OpenBSD__)
52 ip->ip_len = htons(userlen); /* сетевой порядок байтов */
53 #else
54 ip->ip_len = userlen; /* порядок байтов узла */
55 #endif
56 ip->ip_id = 0; /* это пусть устанавливает уровень IP */
57 ip->ip_off = 0; /* смещение флагов, флаги MF и DF */
58 ip->ip_ttl = TTL_OUT;
59 Sendto(rawfd, buf, userlen, 0, dest, destlen);
60 }
Инициализация указателей на заголовки пакетов
24-26 Указатель ip указывает на начало заголовка IP (структуру ip), а указатель ui указывает на то же место, но структура udpiphdr является объединением заголовков IP и UDP.
Обнуление заголовка
27 Мы явным образом записываем в заголовок нули, чтобы предотвратить учет случайного мусора, который мог остаться в буфере, при вычислении контрольной суммы.
Обновление значений длины
28-31 Переменная ui_len — это длина дейтаграммы UDP: количество байтов пользовательских данных плюс размер заголовка UDP (8 байт). Переменная userlen (количество байтов пользовательских данных, которые следуют за заголовком UDP) увеличивается на 28 (20 байт на заголовок IP и 8 байт на заголовок UDP), для того чтобы соответствовать настоящему размеру дейтаграммы IP.
Заполнение заголовка UDP и вычисление контрольной суммы UDP
32-45 При вычислении контрольной суммы UDP учитывается не только заголовок и данные UDP, но и поля заголовка IP. Эти дополнительные поля заголовка IP образуют то, что называется псевдозаголовком (pseudoheader). Включение псевдозаголовка обеспечивает дополнительную проверку на то, что если значение контрольной суммы верно, то дейтаграмма была доставлена на правильный узел и с правильным кодом протокола. В указанных строках располагаются операторы инициализации полей в IP-заголовке, формирующих псевдозаголовок. Данный фрагмент кода несколько запутан, но его объяснение приводится в разделе 23.6 [128]. Конечным результатом является запись контрольной суммы UDP в поле ui_sum, если не установлен флаг zerosum (что соответствует наличию аргумента командной строки -0).
Если при вычислении контрольной суммы получается 0, вместо него записывается значение 0xffff. В обратном коде эти числа совпадают, но протокол UDP устанавливает контрольную сумму в нуль, чтобы обозначить, что она вовсе не была вычислена. Обратите внимание, что в листинге 28.10 мы не проверяем, равно ли значение контрольной суммы нулю: дело в том, что в случае ICMPv4 нулевое значение контрольной суммы не означает ее отсутствия.
ПРИМЕЧАНИЕ
Следует отметить, что в Solaris 2.x, где x<6, в случаях, когда дейтаграммы UDP или сегменты TCP отправляются с символьного сокета при установленном параметре IP_HDRINCL, возникает ошибка. Контрольную сумму вычисляет ядро, а мы должны установить поле ui_sum равным длине дейтаграммы UDP.
Заполнение заголовка IP
36-49 Поскольку мы установили параметр сокета IP_HDRINCL, нам следует заполнить большую часть полей в заголовке IP. (В разделе 28.3 обсуждается запись в символьный сокет при включенном параметре IP_HDRINCL.) Мы присваиваем полю идентификации нуль (ip_id), что указывает IP на необходимость задания значения этого поля. IP также вычисляет контрольную сумму IP, а функция sendto записывает дейтаграмму IP.
ПРИМЕЧАНИЕ
Обратите внимание, что поле ip_len может иметь либо сетевой порядок байтов, либо порядок байтов узла. Это типичная проблема с совместимостью, возникающая при использовании символьных сокетов.
Следующая функция — это udp_read, показанная в листинге 29.11. Она вызывается из кода, представленного в листинге 29.6.
Листинг 29.11. Функция udp_read: чтение очередного пакета из устройства захвата пакетов
//udpcksum/udpread.c
7 struct udpiphdr*
8 udp_read(void)
9 {
10 int len;
11 char *ptr;
12 struct ether_header *eptr;
13 for (;;) {
14 ptr = next_pcap(&len);
15 switch (datalink) {
16 case DLT_NULL: /* заголовок обратной петли = 4 байта */
17 return (udp_check(ptr + 4, len — 4));
18 case DLT_EN10MB:
19 eptr = (struct ether_header*)ptr;
20 if (ntohs(eptr->ether_type) != ETHERTYPE_IP)
21 err_quit("Ethernet type not IP", ntohs(eptr->ether_type));
22 return (udp_check(ptr + 14, len — 14));
23 case DLT_SLIP: /* заголовок SLIP = 24 байта */
24 return (udp_check(ptr + 24, len — 24));
25 case DLT_PPP: /* заголовок PPP = 24 байта */
26 return (udp_check(ptr + 24, len — 24));
27 default:
28 err_quit("unsupported datalink (%d)", datalink);
29 }
30 }
31 }
14-29 Наша функция next_pcap (см. листинг 29.12) возвращает следующий пакет из устройства захвата пакетов. Поскольку заголовки канального уровня различаются в зависимости от фактического типа устройства, мы применяем ветвление в зависимости от значения, возвращаемого функцией pcap_datalink.
ПРИМЕЧАНИЕ
Сдвиги на 4, 14 и 24 байта объясняются на рис. 31.9 [128]. Сдвиг, равный 24 байтам, показанный для заголовков SLIP и PPP, применяется в BSD/OS 2.1.
Несмотря на то, что в названии DLT_EN10MB фигурирует обозначение «10МВ», этот тип канального уровня используется для сетей Ethernet, в которых скорость передачи данных равна 100 Мбит/с.
Наша функция udp_check (см. листинг 29.13) исследует пакет и проверяет поля в заголовках IP и UDP.
В листинге 29.12 показана функция next_pcap, возвращающая следующий пакет из устройства захвата пакетов.
Листинг 29.12. Функция next_pcap: возвращает следующий пакет
//udpcksum/pcap.c
38 char*
39 next_pcap(int *len)
40 {
41 char *ptr;
42 struct pcap_pkthdr hdr;
43 /* продолжаем следить, пока пакет не будет готов */
44 while ((ptr = (char*)pcap_next(pd, &hdr)) == NULL);
45 *len = hdr.caplen; /* длина захваченного пакета */
46 return (ptr);
47 }
43-44 Мы вызываем библиотечную функцию pcap_next, возвращающую следующий пакет. Указатель на пакет является возвращаемым значением данной функции, а второй аргумент указывает на структуру pcap_pkthdr, которая тоже возвращается заполненной:
struct pcap_pkthdr {
struct timeval ts; /* временная метка */
bpf_u_int32 caplen; /* длина захваченного фрагмента */
bpf_u_int32 len; /* полная длина пакета, находящегося в канале */
};
Временная отметка относится к тому моменту, когда пакет был считан устройством захвата пакетов, в противоположность моменту фактической передачи пакета процессу, которая может произойти чуть позже. Переменная caplen содержит длину захваченных данных (вспомним, что в листинге 29.2 нашей переменной shaplen было присвоено значение 200 и она являлась вторым аргументом функции pcap_open_live в листинге 29.5). Назначение устройства захвата пакетов состоит в захвате заголовков, а не всего содержимого каждого пакета. Переменная len — это полная длина пакета, находящегося в канале. Значение caplen будет всегда меньше или равно значению len.
45-46 Перехваченная часть пакета возвращается через указатель (аргумент функции), и возвращаемым значением функции является указатель на пакет. Следует помнить, что указатель на пакет указывает фактически на заголовок канального уровня, который представляет собой 14-байтовый заголовок Ethernet в случае кадра Ethernet или 4-байтовый псевдоканальный (pseudo-link) заголовок в случае закольцовки на себя.
Если мы посмотрим на библиотечную реализацию функции pcap_next, мы увидим, что между различными функциями существует некоторое «разделение труда», схематически изображенное на рис. 29.5. Наше приложение вызывает функции pcap_, среди которых есть как зависящие, так и не зависящие от устройства захвата пакетов. Например, мы показываем, что реализация BPF вызывает функцию read, в то время как реализация DLPI вызывает функцию getmsg, а реализация Linux вызывает recvfrom.
Рис. 29.5. Организация вызовов функций для чтения из библиотеки захвата пакетов
Наша функция udp_check проверяет различные поля в заголовках IP и UDP. Она показана в листинге 29.13. Эту проверку необходимо выполнить, так как при получении пакета от устройства захвата пакетов уровень IP не замечает этого пакета. Для символьного сокета это не так.
44-61 Длина пакета должна включать хотя бы заголовки IP и UDP. Версия IP проверяется вместе с длиной и контрольной суммой заголовка IP. Если поле протокола указывает на дейтаграмму UDP, функция возвращает указатель на объединенный заголовок IP/UDP. В противном случае программа завершается, так как фильтр захвата пакетов, заданный при вызове функции pcap_setfilter в листинге 29.5, не должен возвращать пакеты никакого другого типа.
Листинг 29.13. Функция udp_check: проверка полей в заголовках IP и UDP
//udpcksum/udpread.c
38 struct udpiphdr*
39 udp_check(char *ptr, int len)
40 {
41 int hlen;
42 struct ip *ip;
43 struct udpiphdr *ui;
44 if (len < sizeof(struct ip) + sizeof(struct udphdr))
45 err_quit("len = %d", len);
46 /* минимальная проверка заголовка IP */
47 ip = (struct ip*)ptr;
48 if (ip->ip_v != IPVERSION)
49 err_quit("ip_v = %d", ip->ip_v);
50 hlen = ip->ip_hl << 2;
51 if (hlen < sizeof(struct ip))
52 err_quit("ip_hl = %d", ip->ip_hl);
53 if (len < hlen + sizeof(struct udphdr))
54 err_quit("len = %d, hlen = %d", len, hlen);
55 if ((ip->ip_sum = in_cksum((u_short )ip, hlen)) != 0)
56 err_quit("ip checksum error");
57 if (ip->ip_p == IPPROTO_UDP) {
58 ui = (struct udpiphdr*)ip;
59 return (ui);
60 } else
61 err_quit("not a UDP packet");
62 }
Функция cleanup, показанная в листинге 29.14, вызывается из функции main непосредственно перед тем, как программа завершается, а также вызывается в качестве обработчика сигнала в случае, если пользователь прерывает выполнение программы (см. листинг 29.4).
Листинг 29.14. Функция cleanup
//udpcksum/cleanup.c
2 void
3 cleanup(int signo)
4 {
5 struct pcap_stat stat;
6 fflush(stdout);
7 putc('\n', stdout);
8 if (verbose) {
9 if (pcap_stats(pd, &stat) < 0)
10 err_quit("pcap_stats: %s\n", pcap_geterr(pd));
11 printf("%d packets received by filter\n", stat.ps_recv);
12 printf("%d packets dropped by kernel\n", stat.ps_drop);
13 }
14 exit(0);
15 }
Получение и вывод статистики по захвату пакетов
8-13 Функция pcap_stats получает статистику захвата пакетов: общее количество полученных фильтром пакетов и количество пакетов, переданных ядру.