29.1. Введение
В настоящее время большинство операционных систем позволяют приложению получать доступ к канальному уровню. Это подразумевает следующие возможности:
1. Отслеживание пакетов, принимаемых на канальном уровне, что, в свою очередь, позволяет запускать такие программы, как
tcpdump
, на обычных компьютерных системах (а не только на специальных аппаратных устройствах для отслеживания пакетов). Если добавить к этому способность сетевого интерфейса работать в смешанном режиме (promiscuous mode), приложение сможет отслеживать все пакеты, проходящие по локальному кабелю, а не только предназначенные для того узла, на котором работает эта программа.Эта возможность не так полезна в коммутируемых сетях, которые получили широкое распространение в последнее время. Дело в том, что коммутатор пропускает трафик на конкретный порт только в том случае, если этот трафик адресован конкретному устройству или устройствам, подключенным к этому порту, каким бы трафик ни был: направленным, широковещательным или многоадресным. Для того чтобы получать трафик, передаваемый через другие порты коммутатора, нужно сначала переключить свой порт коммутатора в режим контроля (monitor mode или port mirroring). Заметьте, что многие устройства, которые обычно не считают коммутаторами, на самом деле являются таковыми. Например, двухскоростной концентратор 10/100 обычно является двухпортовым коммутатором: один порт для сетей, работающих на 100 Мбит/с, другой — для сетей на 10 Мбит/с.
2. Возможность запуска определенных программ как обычных приложений, а не как частей ядра. Скажем, большинство версий Unix сервера RARP — это обычные приложения, которые считывают запросы RARP с канального уровня (запросы RARP не являются дейтаграммами IP), а затем передают ответы также на канальный уровень.
Три наиболее распространенных средства получения доступа к канальному уровню в Unix — это пакетный фильтр BSD (BPF, BSD Packet Filter), DLPI в SVR4 (Datalink Provider Interface — интерфейс поставщика канального уровня) и интерфейс пакетных сокетов Linux (
SOCK_PACKET
). Мы приводим в этой главе обзор перечисленных средств, а затем описываем libcap
— открытую для свободного доступа библиотеку, содержащую функции для захвата пакетов. Эта библиотека работает со всеми тремя перечисленными средствами, и использование библиотеки позволяет сделать наши программы не зависящими от фактического способа обеспечения доступа к канальному уровню, применяемому в данной операционной системе. Мы описываем эту библиотеку, разрабатывая программу, которая посылает запросы серверу имен DNS (мы составляем свои собственные дейтаграммы UDP и записываем их в символьный сокет) и считывает ответ при помощи libcap
, чтобы определить, добавляет ли сервер имен контрольную сумму в дейтаграммы UDP.29.2. BPF: пакетный фильтр BSD
4.4BSD и многие другие Беркли-реализации поддерживают BPF — пакетный фильтр BSD (BSD Packet Filter). Реализация BPF описана в главе 31 [128]. История BPF, описание псевдопроцессора BPF и сравнение с пакетным фильтром SunOs 4.1.x NIT приведены в [72].
Каждый канальный уровень вызывает BPF сразу после получения пакета и непосредственно перед его передачей выше, как показано на рис. 29.1.
Рис. 29.1. Захват пакета с использованием BPF
Примеры подобных вызовов для интерфейса Ethernet приведены на рис. 4.11 и 4.19 в [128]. Вызов BPF должен произойти как можно скорее после получения пакета и как можно позже перед его передачей, так как это увеличивает точность временных отметок.
Организовать само по себе перехватывание пакетов из канального уровня не очень сложно, однако преимущество BPF заключается в возможности их фильтрации. Каждое приложение, открывающее устройство BPF, может загрузить свой собственный фильтр, который затем BPF применяет к каждому пакету. В то время как некоторые фильтры достаточно просты (например, при использовании фильтра
udp or tcp
принимаются только пакеты UDP и TCP), другие фильтры позволяют исследовать значения определенных полей в заголовках пакетов. Например, фильтрtcp and port 80 and tcp[13:l] & 0x7 != 0
использовался в главе 14 [128] для отбора сегментов TCP, направлявшихся к порту 80 или от него и содержащих флаги SYN, FIN или RST. Выражение
tcp [13:1]
соответствует однобайтовому значению, начинающемуся с 13-го байта от начала заголовка TCP.В BPF реализован основанный на регистрах механизм фильтрации, который применяет специфические для приложений фильтры к каждому полученному пакету. Хотя можно написать свою программу фильтрации на машинном языке псевдопроцессора (он описан в руководстве по использованию BPF), проще всего будет компилировать строки ASCII (такие, как только что показанная строка, начинающаяся с
tcp
) в машинный язык с помощью функции pcap_compile
, о которой мы рассказываем в разделе 29.7.В технологии BPF применяются три метода, позволяющие уменьшить накладные расходы на ее использование.
1. Фильтрация BPF происходит внутри ядра, за счет чего минимизируется количество данных, которые нужно копировать из ядра в приложение. Копирование из пространства ядра в пользовательское пространство является довольно дорогостоящим. Если бы приходилось копировать каждый пакет, у BPF могли бы возникнуть проблемы при попытке взаимодействия с быстрыми каналами.
2. BPF передает приложению только часть каждого пакета. Здесь речь идет о длине захвата (capture length). Большинству приложений требуется только заголовок пакета, а не содержащиеся в нем данные. Это также уменьшает количество данных, которые BPF должен скопировать в приложение. В программе
tcpdump
, например, по умолчанию это значение равно 68 байт, и этого достаточно для размещения 14-байтового заголовка Ethernet, 20-байтового заголовка IP, 20-байтового заголовка TCP и 14 байт данных. Но для вывода дополнительной информации по другим протоколам (например, DNS или NFS) требуется, чтобы пользователь увеличил это значение при запуске программы tcpdump
.3. BPF буферизует данные, предназначенные для приложения, и этот буфер передается приложению только когда он заполнен или когда истекает заданное время ожидания для считывания (read timeout). Это время может быть задано приложением. Программа
tcpdump
, например, устанавливает время ожидания 1000 мс, а демон RARP задает нулевое время ожидания (поскольку пакетов RARP немного, а сервер RARP должен послать ответ сразу, как только он получает запрос). Назначением буферизации является уменьшение количества системных вызовов. При этом между BPF и приложением происходит обмен тем же количеством пакетов, но за счет того, что уменьшается количество системных вызовов, каждый из которых связан с дополнительными накладными расходами, уменьшается и общий объем этих расходов. Например, на рис. 3.1 [110] сравниваются накладные расходы, возникающие при системном вызове read, когда файл считывается в несколько приемов, причем размер фрагментов варьируется от 1 до 131 072 байт.Хотя на рис. 29.1 мы показываем только один буфер, BPF поддерживает по два внутренних буфера для каждого приложения и заполняет один, пока другой копируется в приложение. Эта стандартная технология носит название двойной буферизации (double buffering).
На рис. 29.1 мы показываем только получение пакетов фильтром BPF: пакеты, приходящие на канальный уровень снизу (из сети) и сверху (IP). Приложение также может записывать в BPF, в результате чего пакеты будут отсылаться по канальному уровню, но большая часть приложений только считывает пакеты из BPF. У нас нет оснований использовать BPF для отправки дейтаграмм IP, поскольку параметр сокета
IP_HDRINCL
позволяет нам записывать дейтаграммы IP любого типа, включая заголовок IP. (Подобный пример мы показываем в разделе 29.7.) Записывать в BPF можно только с одной целью — чтобы отослать наши собственные сетевые пакеты, не являющиеся дейтаграммами IP. Например, демон RARP делает это для отправки ответов RARP, которые не являются дейтаграммами IP.Для получения доступа к BPF необходимо открыть (вызвав функцию
open
) еще не открытое каким-либо другим процессом устройство BPF. Скажем, можно попробовать /dev/bpf0
, и если будет возвращена ошибка EBUSY
, то — /dev/bpf1
, и т.д. Когда устройство будет открыто, потребуется выполнить примерно 12 команд ioctl
для задания характеристик устройства, таких как загрузка фильтра, время ожидания для считывания, размер буфера, присоединение канального уровня к устройству BPF, включение смешанного режима, и т.д. Затем с помощью функций read
и write
осуществляется ввод и вывод.29.3. DLPI: интерфейс поставщика канального уровня
SVR4 обеспечивает доступ к канальному уровню через DLPI (Data Link Provider Interface — интерфейс поставщика канального уровня). DLPI — это не зависящий от протокола интерфейс, разработанный в AT&T и служащий средством связи с сервисами, обеспечиваемыми канальным уровнем [124]. Доступ к DLPI осуществляется посредством отправки и получения сообщений через потоки STREAMS.
Для подсоединения к канальному уровню приложение просто открывает устройство (например,
le0
) с помощью команды open и использует запрос DL_ATTACH_REQ
. Но для эффективной работы используются два дополнительных модуля: pfmod
, который осуществляет фильтрацию внутри ядра, и bufmod
, буферизующий данные, предназначенные для приложения. Это показано на рис. 29.2.Рис. 29.2. Захват пакета с использованием DLPI, pfmod и bufmod
Концептуально DLPI аналогичен BPF.
pfmod
поддерживает фильтрацию внутри ядра, используя псевдопроцессор, a bufmod
сокращает количество данных и системных вызовов, поддерживая длину захвата и время ожидания для считывания.Одно интересное различие, тем не менее, заключается в том, что для BPF и фильтров
pfmod
используются разные типы псевдопроцессоров. Фильтр BPF — это ориентированный ациклический граф управления потоком (acyclic control flow graph, CFG), в то время как pfmod
использует дерево булевых выражений. В первом случае естественным является отображение в код для вычислительной машины с регистровой организацией, а во втором — в код для машины со стековой организацией [72]. В статье [72] показано, что реализация CFG, используемая в BPF, обычно работает быстрее, чем дерево булевых выражений, в 3-20 раз в зависимости от сложности фильтра.Еще одно отличие состоит в том, что BPF всегда выполняет фильтрацию перед копированием пакета, чтобы не копировать те пакеты, которые будут сброшены фильтром. В некоторых реализациях DLPI пакеты сначала копируются в модуль
pfmod
, который затем может сбрасывать их.29.4. Linux: SOCK_PACKET и PF_PACKET
Существует два метода получения пакетов канального уровня в Linux. Первоначальный метод получил более широкое распространение, но является менее гибким. Он состоит в создании сокета типа
SOCK_PACKET
. Новый метод, предоставляющий больше возможностей для настройки фильтров и оптимизации производительности, состоит в создании сокета семейства PF_PACKET
. В любом случае мы должны обладать правами привилегированного пользователя (аналогичные необходимым для создания символьного сокета), а третий аргумент функции socket должен быть ненулевым значением, задающим тип кадра Ethernet. При использовании сокетов PF_PACKET
второй аргумент socket
может быть константой SOCK_DGRAM
(для получения обработанных пакетов без заголовка канального уровня) или SOCK_RAW
(для получения пакетов целиком). Сокеты SOCK_PACKET
передают пакеты только целиком. Например, для получения всех кадров канального уровня мы пишем:fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); /* в новых системах */
или
fd = socket(AF_INET, SOCK_PACKET, htons(ETH_P_ALL)); /* в старых системах */
В результате этого будут возвращены кадры для всех протоколов, получаемые канальным уровнем. Если нам нужны кадры IPv4, то вызов будет таким:
fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP)); /* в новых системах */
fd = socket(AF_INET, SOCK_PACKET, htons(ETH_P_IP)); /* в старых системах */
Другие константы, которые могут использоваться в качестве последнего аргумента, — это, например,
ETH_P_ARP
и ETH_P_IPV6
.Указывая протокол
ETH_P_ххх
, мы тем самым сообщаем канальному уровню, какой тип из получаемых канальным уровнем кадров передавать сокету. Если канальный уровень поддерживает смешанный режим (например, Ehternet), то устройство тоже должно работать в смешанном режиме. Это осуществляется при помощи параметра сокета PACKET_ADD_MEMBERSHIP
с использованием структуры packet_mreq
. При этом необходимо указать конкретный интерфейс и задать тип действия PACKET_MR_PROMISC
. В старых системах для этого нужно вызвать функцию ioctl
с запросом SIOCGIFFLAGS
для получения флагов, установить флаг IFF_PROMISC
и далее сохранить флаги с помощью SIOCSIFFLAGS
. К сожалению, при использовании этого метода программы, работающие в смешанном режиме, могут мешать друг другу, а если в одной из них содержатся ошибки, то она может и не отключить смешанный режим по завершении.Сравнивая это средство Linux с BPF и DLPI, мы можем отметить некоторые различия.
1. В Linux не обеспечивается буферизация. Фильтрация на уровне ядра доступна только в новых системах (при помощи параметра
SO_ATTACH_FILTER
). Существует обычный буфер приема сокета, но отсутствует возможность буферизации и отправки приложению нескольких кадров с помощью одной операции считывания. Это увеличивает накладные расходы, связанные с копированием потенциально возможных больших объемов данных из ядра в приложение.2. В Linux не предусмотрена фильтрация на уровне устройства. Сокеты
PF_PACKET
могут быть связаны с устройством функцией bind
. Если в вызове функции socket
указан аргумент ETH_P_IP
, то все пакеты IPv4 со всех устройств (например, Ethernet, каналы PPP, каналы SLIP и закольцовка) будут переданы на сокет. Функция recvfrom
возвращает общую структуру адреса сокета, а элемент sa_data
содержит имя устройства (например, eth0
). Тогда приложение само должно игнорировать данные с тех устройств, которые не представляют для него интереса. Здесь мы сталкиваемся фактически с той же проблемой: возможно, что приложение будет получать слишком много данных, особенно в случае наблюдения за высокоскоростной сетью.29.5. Libcap: библиотека для захвата пакетов
Библиотека захвата пакетов
libcap
обеспечивает не зависящий от реализации доступ к средствам операционной системы, с помощью которых осуществляется этот захват. В настоящее время поддерживается только чтение пакетов (хотя добавление нескольких строк кода в библиотеку позволяет также записывать пакеты в некоторых системах). В следующем разделе приводится описание альтернативной библиотеки, которая не только дает возможность записывать пакеты на канальный уровень, но и позволяет конструировать пакеты произвольного типа.Сейчас осуществляется поддержка BPF для Беркли-ядер, DLPI для Solaris 2.x, NIT для SunOS 4.1.x, пакетных сокетов (
SOCK_PACKET
, PF_PACKET
) в Linux и нескольких других операционных системах. Библиотека libcap
используется программой tcpdump
. Всего в библиотеке насчитывается порядка 25 функций, но вместо того чтобы просто описывать их, мы продемонстрируем их фактическое использование на примере, рассматриваемом в следующем разделе. Названия всех функций начинаются с pcap_
. Они описаны более подробно на странице руководства, которая называется pcap
.Библиотека libcap находится в свободном доступе по адресу http://www.tcpdump.org/.
29.6. Libnet: библиотека создания и отправки пакетов
Библиотека
libnet
предоставляет интерфейс для создания и отправки в сеть пакетов произвольного содержимого. Она обеспечивает доступ на уровне символьных сокетов и доступ к канальному уровню в формате, не зависящем от реализации.Библиотека скрывает большую часть деталей формирования заголовков IP, UDP и TCP и обеспечивает приложению простой и переносимый интерфейс для отправки пакетов канального уровня и IP-пакетов через символьные сокеты. Как и
libcap
, библиотека libnet
содержит достаточно много функций. Мы приведем пример использования небольшой их части, предназначенной для работы с символьными сокетами, но в следующем разделе. Для сравнения там же будет приведен код, непосредственно работающий с символьными сокетами. Все функции библиотеки начинаются с префикса libnet_
. За более подробным их описанием вы можете обратиться к странице руководства libnet
или к доступной в Сети документации.Библиотека libnet свободно доступна по адресу http://www.packetfactory.net/libnet/. Руководство находится по адресу http://www.packetfactory.net/libnet/manual. На момент написания этой книги в Сети имелось руководство только по устаревшей версии 1.0. Актуальная версия 1.1 имеет значительно отличающийся интерфейс. В нашем примере используется API версии 1.1.
29.7. Анализ поля контрольной суммы UDP
Теперь мы приступаем к рассмотрению примера, в котором отсылается дейтаграмма 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[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 }
15
Нам нужно, чтобы две динамические локальные переменные nsent
и timeout
сохраняли свои значения после возвращения siglongjmp
из обработчика сигнала в нашу функцию. Реализация допускает восстановление значений динамических локальных переменных, предшествовавших вызову функции sigsetjump
[110, с. 178], но добавление спецификатора volatile
предотвращает это восстановление.15-16
Для сигнала SIGALRM
устанавливается обработчик сигнала, а функция sigsetjmp
устанавливает буфер перехода для функции siglongjmp
. (Эти две функции подробно описаны в разделе 10.15 [110].) Значение 1 во втором аргументе функции sigsetjmp
указывает, что требуется сохранить текущую маску сигнала, так как мы будем вызывать функцию siglongjmp
из нашего обработчика сигнала.19-23
Этот фрагмент кода выполняется, только когда функция siglongjmp
вызывается из нашего обработчика сигнала. Вызов указывает на возникновение условий, при которых мы входим в состояние ожидания: мы отправили запрос, на который не пришло никакого ответа. Если после того, как мы отправим три запроса, ответа не будет, мы прекращаем выполнение кода. По истечении времени ожидания, отведенного на получение ответа, мы выводим соответствующее сообщение и увеличиваем значение времени ожидания в два раза, то есть задаем экспоненциальное смещение (exponential backoff), которое также описано в разделе 20.5. Первое значение времени ожидания равно 3 с, затем — 6 с и 12 с.Причина, по которой в этом примере мы используем функции
sigsetjmp
и siglongjmp
, вместо того чтобы просто перехватывать ошибку EINTR
(как мы поступили в листинге 14.1), заключается в том, что библиотечные функции захвата пакетов (которые вызываются из нашей функции udp_read
) заново запускают операцию чтения в случае возвращения ошибки EINTR
. Поскольку мы не хотим модифицировать библиотечные функции, единственным решением для нас является перехватывание сигнала SIGALRM
и выполнение нелокального перехода (оператора goto
), который возвращает управление в наш код, а не в библиотечную функцию.25-26
Функция send_dns_query
(см. листинг 29.8) отправляет запрос DNS на сервер имен. Функция dns_read
считывает ответ. Мы вызываем функцию alarm
для предотвращения «вечной» блокировки функции read
. Если истекает заданное (в секундах) время ожидания, генерируется сигнал SIGALRM
, и наш обработчик сигнала вызывает функцию siglongjmp
.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
установлен на первый байт пользовательских данных.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.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
Мы объявляем глобальную переменную, в которой будет храниться дескриптор символьного сокета.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.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.
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
получает статистику захвата пакетов: общее количество полученных фильтром пакетов и количество пакетов, переданных ядру.Пример
Сначала мы запустим нашу программу с аргументом командной строки
-0
и убедимся, что сервер имен отвечает на приходящие дейтаграммы, не содержащие контрольной суммы. Мы также задаем флаг -v
.macosx # udpcksum -i en1 -0 -v bridget.rudoff.com domain
device = en1
local net = 172.24.37.64. netmask = 255.255.255.224
cmd = udp and src host 206.168.112.96 and src port 53
datalink = 1
sent: 36 bytes of data
UDP checksums on
received UDP checksum = 9d15
3 packets received by filter
0 packets dropped by kernel
Затем мы запускаем нашу программу, обращаясь к локальному серверу имен, в котором отключен подсчет контрольных сумм. Чем дальше, тем сложнее становится найти сервер имен с отключенным подсчетом контрольных сумм.
macosx # udpcksum -i en1 -v freebsd4.unpbook.com domain
device = en1
localnet = 172.24.37.64, netmask = 255.255.255.224
cmd = udp and src host 172.24.37.94 and src port 53
datalink = 1
sent: 36 bytes of data
UDP checksums off
received UDP checksum = 0
3 packets received by filter
0 packets dropped by kernel
Функции libnet
В этом разделе приводятся альтернативные версии функций
open_output
и send_dns_query
, в которых вместо символьных сокетов используются функции библиотеки libnet
. Библиотека libnet
берет на себя заботу о множестве деталей, в частности, устраняет проблемы с переносимостью, связанные с вычислением контрольных сумм и порядком байтов в заголовке, о которых мы говорили выше. Функция open output представлена в листинге 29.15.Листинг 29.15. Функция open_output, использующая libnet
//udpcksum/senddnsquery-libnet.c
7 static libnet_t *l; /* дескриптор libnet */
8 void
9 open_output(void)
10 {
11 char errbuf[LIBNET_ERRBUF_SIZE];
12 /* инициализация libnet с символьным сокетом IPv4 */
13 l = libnet_init(LIBNET_RAW4, NULL, errbuf);
14 if (l == NULL) {
15 err_quit("Can't initialize libnet: %s", errbuf);
16 }
17 }
7
В библиотеке libnet
используется непрозрачный тип libnet_t
. Функция libnet_init
возвращает указатель на этот тип, который затем передается другим функциям libnet
для обращения к конкретному сокету. В этом смысле данный тип аналогичен дескрипторам сокетов и устройств pcap
.12-16
Мы вызываем функцию libnet_init
, запрашивая открытие символьного сокета IPv4. Для этого в качестве первого аргумента указывается константа LIBNET_RAW4
. В случае возникновения ошибки функция возвращает текст сообщения в аргументе errbuf
, который мы распечатываем.Функция
send_dns_query
для libnet
представлена в листинге 29.16. Сравните ее с функциями send_dns_query
и udp_write
для символьных сокетов.Листинг 29.16. Функция send_dns_query, использующая libnet
//udpcksum/senddnsquery-libnet.c
18 void
19 send_dns_query(void)
20 {
21 char qbuf[24], *ptr;
22 u_int16_t one;
23 int packet_size = LIBNET_UDP_H + LIBNET_DNSV4_H + 24;
24 static libnet_ptag_t ip_tag, udp_tag, dns_tag;
25 /* построение запроса внутри UDP-пакета */
26 ptr = qbuf;
27 memcpy(ptr, "\001a\014root-servers\003net\000", 20);
28 ptr += 20;
29 one = htons(1);
30 memcpy(ptr, &one, 2); /* тип запроса = A */
31 ptr += 2;
32 memcpy(ptr, &one, 2); /* класс запроса = 1 (IP-адрес) */
33 /* формирование пакета DNS */
34 dns_tag = libnet_build_dnsv4(
35 1234 /* идентификатор */,
36 0x0100 /* флаги: рекурсия разрешена */,
37 1 /* кол-во запросов */, 0 /* кол-во записей в ответе */,
38 0 /* кол-во авторитетных записей */, 0 /* кол-во дополнительных */,
39 qbuf /* запрос */,
40 24 /* длина запроса */, 1, dns_tag);
41 /* формирование заголовка UDP */
42 udp_tag = libnet_build_udp(
43 ((struct sockaddr_in*)local)->
44 sin_port /* порт отправителя */,
45 ((struct sockaddr_in*)dest)->
46 sin_port /* порт получателя */,
47 packet_size /* длина */, 0 /* контрольная сумма */,
48 NULL /* полезные данные */, 0 /* длина полезн. данных */, l, udp_tag);
49 /* Так как мы установили контр. сумму равной нулю, libnet автоматически */
50 /* рассчитает контр. сумму UDP. Эту функцию можно отключить. */
51 if (zerosum)
52 if (libnet_toggle_checksum(l, udp_tag, LIBNET_OFF) < 0)
53 err_quit("turning off checksums: %s\n", libnet_geterror(l));
54 /* формирование IP-заголовка */
55 ip_tag = libnet_build_ipv4(packet_size + LIBNET_IPV4_H /* длина */,
56 0 /* tos */, 0 /* IP ID */, 0 /* фрагмент*/,
57 TTL_OUT /* ttl */, IPPROTO_UDP /* протокол */,
58 0 /* контр. сумма */,
59 ((struct sockaddr_in*)local)->sin_addr.s_addr /* отправитель */,
60 ((struct sockaddr_in*)dest)->sin_addr.s_addr /* получатель */,
61 NULL /* полезные данные */, 0 /* длина полезн. данных */, l, ip_tag);
62 if (libnet_write(l) < 0) {
63 err_quit("libnet_write: %s\n", libnet_geterror(l));
64 }
65 if (verbose)
66 printf("sent: %d bytes of data\n", packet_size);
67 }
25-32
Мы начинаем с формирования запроса DNS, которое выполняется так же, как в строках 25–30 листинга 29.8.34-40
Затем мы вызываем функцию libnet_build_dnsv4
, которая принимает поля пакета DNS в виде отдельных аргументов. Нам достаточно знать содержимое запроса, а упорядочением этого содержимого в заголовке пакета DNS занимается функция.42-48
Мы формируем заголовок UDP, вызывая функцию libnet_build_udp
. Поля заголовка UDP принимаются этой функцией также в виде отдельных аргументов. Если значение переданной контрольной суммы равно 0, libnet
автоматически рассчитывает контрольную сумму.49-52
Если пользователь запретил вычисление контрольной суммы, мы должны отключить эту функцию libnet
явным образом.53-65
Окончательное формирование пакета требует построения заголовка IPv4 вызовом libnet_build_ipv4
.Библиотека libnet автоматически записывает поле ip_len в нужном порядке байтов. Это пример повышения переносимости программы благодаря использованию библиотек.
66-70
Мы вызываем функцию libnet_write
для отправки подготовленной дейтаграммы в сеть.Функция
send_dns_query
, использующая libnet
, состоит всего из 67 строк, тогда как в версии, работавшей с символьными сокетами, общая длина кода составила 96 строк, в которых было по крайней мере 2 трюка, связанных с переносимостью.29.8. Резюме
Символьные сокеты предоставляют возможность записывать и считывать IP-дейтаграммы, которые могут быть не поняты ядром, а доступ к канальному уровню позволяет считывать и записывать кадры канального уровня любых типов (не только дейтаграммы IP). Программа
tcpdump
— это, вероятно, наиболее широко используемая программа, имеющая непосредственный доступ к канальному уровню.В различных операционных системах применяются различные способы доступа к канальному уровню. Мы рассмотрели пакетный фильтр Беркли, DLPI SVR4 и пакетные сокеты Linux (
SOCK_PACKET
). Но у нас имеется возможность, не вникая в различия перечисленных способов, использовать находящуюся в свободном доступе переносимую библиотеку захвата пакетов libcap
.Отправка символьных дейтаграмм осуществляется в разных системах по-разному. Свободно распространяемая библиотека
libnet
скрывает различия между системами и предоставляет интерфейс для вывода через символьные сокеты и непосредственно на канальном уровне.Упражнения
1. Каково назначение флага
canjump
в листинге 29.7?2. При работе программы
udpcksum
наиболее распространенным сообщением об ошибке является сообщение о недоступности порта ICMP (в пункте назначения не работает сервер имен) или недоступности узла ICMP. В обоих случаях нам не нужно ждать истечения времени ожидания, заданного функцией udp_read
в листинге 29.6, так как сообщение о подобной ошибке фактически является ответом на наш запрос DNS. Модифицируйте программу таким образом, чтобы она перехватывала эти ошибки ICMP.Глава 30