UNIX: разработка сетевых приложений — страница 30 из 42

Символьные сокеты

28.1. Введение

Символьные, или неструктурированные, сокеты (raw sockets) обеспечивают три возможности, не предоставляемые обычными сокетами TCP и UDP.

1. Символьные сокеты позволяют читать и записывать пакеты ICMPv4, IGMPv4 и ICMPv6. Например, программа

ping
посылает эхо-запросы ICMP и получает эхо-ответы ICMP. (Наша оригинальная версия программы
ping
приведена в разделе 28.5.) Демон маршрутизации многоадресной передачи
mrouted
посылает и получает пакеты IGMPv4.

2. Эта возможность также позволяет реализовывать как пользовательские процессы те приложения, которые построены с использованием протоколов ICMP и IGMP, вместо того чтобы помещать большее количество кода в ядро. Например, подобным образом построен демон обнаружения маршрутов (

in.rdisc
в системе Solaris 2.x. В приложении F книги [111] рассказывается, как можно получить исходный код открытой версии). Этот демон обрабатывает два типа сообщений ICMP, о которых ядро ничего не знает (извещение маршрутизатора и запрос маршрутизатору).

С помощью символьных сокетов процесс может читать и записывать IPv4-дейтаграммы с полем протокола IPv4, которое не обрабатывается ядром. Посмотрите еще раз на 8-разрядное поле протокола IPv4, изображенное на рис. А.1. Большинство ядер обрабатывают дейтаграммы, содержащие значения поля протокола 1 (ICMP), 2 (IGMP), 6 (TCP) и 17 (UDP). Но для этого поля определено гораздо большее количество значений, полный список которых приведен в реестре IANA «Номера протоколов» (Protocol Numbers). Например, протокол маршрутизации OSPF не использует протоколы TCP или UDP, а работает напрямую с протоколом IP, устанавливая в поле протокола значение 89 для IP-дейтаграмм. Программа

gated
, реализующая OSPF, должна использовать для чтения и записи таких IP-дейтаграмм символьный сокет, поскольку они содержат значение поля протокола, о котором ничего не известно ядру. Эта возможность также переносится в версию IPv6.

3. С помощью символьных сокетов процесс может построить собственный заголовок IPv4 при помощи параметра сокета

IP_HDRINCL
. Такую возможность имеет смысл использовать, например, для построения собственного пакета UDP или TCP. Подобный пример приведен в разделе 29.7.

В данной главе описывается создание символьных сокетов, а также операции ввода и вывода с этими сокетами. Далее приводятся версии программ

ping
и
traceroute
, работающие как с версией IPv4, так и с версией IPv6.

28.2. Создание символьных сокетов

При создании символьных сокетов выполняются следующие шаги:

1. Символьный сокет создается функцией

socket
со вторым аргументом
SOCK_RAW
. Третий аргумент (протокол) обычно ненулевой. Например, для создания символьного сокета IPv4 следует написать:

int sockfd;

sockfd = socket(AF_INET, SOCK_RAW, protocol);

где

protocol
— одна из констант
IPPROTO_xxx
, определенных в подключенном заголовочном файле
, например
IPPROTO_ICMP
.

Только привилегированный пользователь может создать символьный сокет. Такой подход предотвращает отправку IP-дейтаграмм в сеть обычными пользователями.

2. Параметр сокета

IP_HDRINCL
может быть установлен следующим образом:

const int on = 1;

if (setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0)

 обработка ошибки

В следующем разделе описывается действие этого параметра.

3. На символьном сокете можно вызвать функцию

bind
, но это делается редко. Эта функция устанавливает только локальный адрес: на символьном сокете нет понятия порта. Что касается вывода, вызов функции
bind
устанавливает IP-адрес отправителя, который будет использоваться для дейтаграмм, отправляемых на символьном сокете (только если не установлен параметр сокета
IP_HDRINCL
). Если функция
bind
не вызывается, ядро использует в качестве IP-адреса отправителя основной IP-адрес исходящего интерфейса.

4. На символьном сокете можно вызвать функцию

connect
, но это делается редко. Эта функция устанавливает только внешний адрес, так как на символьном сокете нет понятия порта. О выводе можно сказать, что вызов функции connect позволяет нам вызвать функцию
write
или
send
вместо
sendto
, поскольку IP-адрес получателя уже определен.

28.3. Вывод на символьном сокете

Вывод на символьном сокете регулируется следующими правилами:

1. Стандартный вывод выполняется путем вызова функции

sendto
или
sendmsg
и определения IP-адреса получателя. Функции
write
,
writev
и
send
также можно использовать, если сокет был присоединен.

2. Если не установлен параметр сокета

IP_HDRINCL
, то начальный адрес данных, предназначенных для записи ядром, указывает на первый байт, следующий за IP-заголовком, поскольку ядро будет строить IP-заголовок и добавлять его к началу данных из процесса. Ядро устанавливает поле протокола создаваемого заголовка IPv4 равным значению третьего аргумента функции
socket
.

3. Если параметр сокета

IP_HDRINCL
установлен, то начальный адрес данных, предназначенных для записи ядром, указывает на первый байт IP-заголовка. Размер данных для записи должен включать размер IP-заголовка вызывающего процесса. Процесс полностью формирует IP-заголовок, за исключением того, что, во-первых, значение поля идентификации IPv4 может быть нулевым (что указывает ядру на необходимость самостоятельно установить это значение), во-вторых, ядро всегда вычисляет и сохраняет контрольную сумму заголовка IPv4, в-третьих, включает или не включает параметры IP (см. раздел 27.2).

4. Ядро фрагментирует символьные пакеты, превышающие значение MTU исходящего интерфейса.

ПРИМЕЧАНИЕ

Согласно документации, символьные сокеты должны предоставлять протоколу такой же интерфейс, как если бы он был реализован в ядре [74]. К сожалению, это означает, что некоторые части интерфейса зависят от ядра операционной системы. В частности, это относится к порядку байтов полей заголовка IP. В Беркли-ядрах все поля имеют порядок байтов сети, за исключением полей ip_len и ip_off, имеющих порядок байтов узла [128, с. 233, с. 1057]. В системах Linux и OpenBSD все поля имеют порядок байтов сети.

Параметр сокета IP_HDRINCL впервые был представлен в системе 4.3BSD Reno. До этого приложение имело единственную возможность определить свой собственный IP- заголовок в пакетах, отсылаемых на символьный сокет, — использовать заплату ядра (kernel patch), которая была представлена в 1988 году Ван Якобсоном (Van Jacobson) для поддержки программы traceroute. Эта заплата позволяла приложению создавать символьный IP-сокет, определяя протокол как IPPROTO_RAW, что соответствовало значению 255 (это значение является зарезервированным и никогда не должно появляться в поле протокола IP-заголовка).

Функции, осуществляющие ввод-вывод на символьном сокете, являются одними из простейших функций в ядре. Например, в книге [128, с. 1054–1057] каждая такая функция занимает около 40 строк кода на языке С. Для сравнения: функция ввода TCP содержит около 2000 строк, а функция вывода TCP около 700 строк.

Приводимое в этой книге описание параметра сокета

IP_HDRINCL
относится к системе 4.4BSD. В более ранних версиях, таких как Net/2, при использовании данного параметра заполнялось большее количество полей заголовка IP.

В протоколе IPv4 пользовательский процесс отвечает за вычисление и установку контрольной суммы любого заголовка, следующего за заголовком IPv4. Например, в нашей программе

ping
(см. листинг 28.10), прежде чем вызывать функцию
sendto
, мы должны вычислить контрольную сумму ICMPv4 и сохранить ее в заголовке ICMPv4.

Особенности символьного сокета версии IPv6

Для символьного сокета IPv6 существуют несколько отличий (RFC 3542 [114]).

■ Все поля в заголовках протоколов, отсылаемых или получаемых на символьном сокете IPv6, должны находиться в сетевом порядке байтов.

■ В IPv6 не существует параметров, подобных параметру

IP_HDRINCL
сокета IPv4. Полные пакеты IPv6 (включая дополнительные заголовки) не могут быть прочитаны или записаны через символьный сокет IPv6. Приложения имеют доступ почти ко всем полям заголовка IPv6 и дополнительных заголовков через параметры сокета или вспомогательные данные (см. упражнение 28.1). Если приложению все же необходимо полностью считать или записать IPv6-дейтаграмму, необходимо использовать доступ к канальному уровню (о нем речь пойдет в главе 29).

■ Как вскоре будет показано, на символьном сокете IPv6 по-другому обрабатываются контрольные суммы.

Параметр сокета IPV6_CHECKSUM

Для символьного сокета ICMPv6 ядро всегда вычисляет и сохраняет контрольную сумму в заголовке ICMPv6, тогда как для символьного сокета ICMPv4 приложение должно выполнять данную операцию самостоятельно (сравните листинги 28.10 и 28.12). И ICMPv4, и ICMPv6 требуют от отправителя вычисления контрольной суммы, но ICMPv6 включает в свою контрольную сумму псевдозаголовок (понятие псевдозаголовка обсуждается при вычислении контрольной суммы UDP в листинге 29.10). Одно из полей этого псевдозаголовка представляет собой IPv6-адрес отправителя, и обычно приложение оставляет ядру возможность выбирать это значение. Чтобы приложению не нужно было пытаться отыскать этот адрес только для вычисления контрольной суммы, проще разрешить вычислять контрольную сумму ядру.

Для других символьных сокетов IPv6 (при создании которых третий аргумент функции

socket
отличен от
IPPROTO_ICMPV6
) параметр сокета сообщает ядру, вычислять ли контрольную сумму и сохранять ли ее в исходящих пакетах, а также следует ли проверять контрольную сумму в приходящих пакетах. По умолчанию этот параметр выключен, а включается он путем присваивания неотрицательного значения параметра, как в следующем примере:

int offset = 2;

if (setsockopt(sockfd, IPPROTO_IPV6, IPV6_CHECKSUM,

&offset, sizeof(offset)) < 0)

 обработка ошибки

Здесь не только разрешается вычисление контрольной суммы на данном сокете, но и сообщается ядру смещение 16-разрядной контрольной суммы в байтах: в данном примере оно составляет два байта от начала данных приложения. Чтобы отключить данный параметр, ему нужно присвоить значение -1. Если он включен, ядро будет вычислять и сохранять контрольную сумму для исходящих пакетов, посланных на данном сокете, а также проверять контрольную сумму для пакетов, получаемых данным сокетом.

28.4. Ввод через символьный сокет

Первый вопрос, на который следует ответить, говоря о символьных сокетах, следующий: какие из полученных IP-дейтаграмм ядро передает символьному сокету? Применяются следующие правила:

1. Получаемые пакеты UDP и TCP никогда не передаются на символьный сокет. Если процесс хочет считать IP-дейтаграмму, содержащую пакеты UDP или TCP, пакеты должны считываться на канальном уровне, как показано в главе 29.

2. Большинство ICMP-пакетов передаются на символьный сокет, после того как ядро заканчивает обработку ICMP-сообщения. Беркли-реализации посылают все получаемые ICMP-пакеты на символьный сокет, кроме эхо-запроса, запроса отметки времени и запроса маски адреса [128, с. 302–303]. Эти три типа ICMP-сообщений полностью обрабатываются ядром.

3. Все IGMP-пакеты передаются на символьный сокет, после того как ядро заканчивает обработку IGMP-сообщения.

4. Все IP-дейтаграммы с таким значением поля протокола, которое не понимает ядро, передаются на символьный сокет. Для этих пакетов ядро выполняет только минимальную проверку некоторых полей IP-заголовка, таких как версия IP, контрольная сумма IPv4-заголовка, длина заголовка и IP-адрес получателя [128, с. 213–220].

5. Если дейтаграмма приходит фрагментами, символьному сокету ничего не передается, до тех пор, пока все фрагменты не прибудут и не будут собраны вместе.

Если у ядра есть IP-дейтаграмма для пересылки символьному сокету, в поисках подходящих сокетов проверяются все символьные сокеты всех процессов. Копия IP-дейтаграммы доставляется каждому подходящему сокету. Для каждого символьного сокета выполняются три перечисленных ниже проверки, и только в том случае, если все три проверки дают положительный результат, дейтаграмма направляется данному сокету.

1. Если при создании символьного сокета определено ненулевое значение

protocol
(третий аргумент функции
socket
), то значение поля протокола полученной дейтаграммы должно совпадать с этим ненулевым значением, иначе дейтаграмма не будет доставлена на данный сокет.

2. Если локальный IP-адрес связан с символьным сокетом функцией

bind
, IP-адрес получателя в полученной дейтаграмме должен совпадать с этим адресом, иначе дейтаграмма не посылается данному сокету.

3. Если для символьного сокета был определен внешний адрес с помощью функции

connect
, IP-адрес отправителя в полученной дейтаграмме должен совпадать с этим адресом, иначе дейтаграмма не посылается данному сокету.

Следует отметить, что если символьный сокет создан с нулевым значением аргумента

protocol
и не вызывается ни функция
bind
, ни функция
connect
, то сокет получает копии всех дейтаграмм, которые ядро направляет символьным сокетам.

Дейтаграммы IPv4 всегда передаются через символьные сокеты целиком, вместе с заголовками. В версии IPv6 символьному сокету передается все, кроме дополнительных заголовков (см., например, рис. 28.4 и 28.6).

ПРИМЕЧАНИЕ

В заголовке IPv4, передаваемом приложению, для ip_len, ip_off и ip_id установлен порядок байтов узла, а все остальные ноля имеют порядок байтов сети. В системе Linux все поля остаются в сетевом порядке байтов.

Как уже говорилось, интерфейс символьных сокетов определяется таким образом, чтобы работа со всеми протоколами, в том числе и не обрабатываемыми ядром, осуществлялась одинаково. Поэтому содержимое полей зависит от ядра операционной системы.

В предыдущем разделе мы отметили, что все ноля символьного сокета IPv6 остаются в сетевом порядке байтов.

Фильтрация по типу сообщений ICMPv6

Символьный сокет ICMPv4 получает большинство сообщений ICMPv4, полученных ядром. Но ICMPv6 является расширением ICMPv4, включающим функциональные возможности ARP и IGMP (см. раздел 2.2). Следовательно, символьный сокет ICMPv6 потенциально может принимать гораздо больше пакетов по сравнению с символьным сокетом ICMPv4. Но большинство приложений, использующих символьные сокеты, заинтересованы только в небольшом подмножестве всех ICMP-сообщений.

Для уменьшения количества пакетов, передаваемых от ядра к приложению через символьный ICMPv6-сокет, предусмотрен фильтр, связанный с приложением. Фильтр объявляется с типом данных

struct icmp6_filter
, который определяется в заголовочном файле
. Для установки и получения текущего ICMPv6-фильтра для символьного сокета ICMPv6 используются функции
setsockopt
и
getsockopt
с аргументом
level
, равным
IPPROTO_ICMPV6
, и аргументом
optname
, равным
ICMP6_FILTER
.

Со структурой

icmp6_filter
работают шесть макросов.

#include 


void ICMP6_FILTER_SETPASSALL(struct icmp6_filter *filt);

void ICMP6_FILTER_SETBLOCKALL(struct icmp6_filter *filt);

void ICMP6_FILTER_SETPASS(int msgtype, struct icmp6_filter *filt);

void ICMP6_FILTER_SETBLOCK(int msgtype, struct icmp6_filter *filt);

int ICMP6_FILTER_WILLPASS(int msgtype, const struct icmp6_filter *filt);

int ICMP6_FILTER_WILLBLOCK(int msgtype, const struct icmp6_filter *filt);

Все возвращают: 1, если фильтр пропускает (блокирует) сообщение данного типа, 0 в противном случае

Аргумент

filt
всех макрокоманд является указателем на переменную
icmp6_filter
, изменяемую первыми четырьмя макрокомандами и проверяемую последними двумя. Аргумент
msgtype
является значением в интервале от 0 до 255, определяющим тип ICMP-сообщения.

Макрокоманда

SETPASSALL
указывает, что все типы сообщений должны пересылаться приложению, а макрокоманда
SETBLOCKALL
— что никакие сообщения не должны посылаться приложениям. По умолчанию при создании символьного сокета ICMPv6 подразумевается, что все типы ICMP-сообщений пересылаются приложению.

Макрокоманда

SETPASS
определяет конкретный тип сообщений, который должен пересылаться приложению, а макрокоманда
SETBLOCK
блокирует один конкретный тип сообщений. Макрокоманда
WILLPASS
возвращает значение 1, если определенный тип пропускается фильтром. Макрокоманда
WILLBLOCK
возвращает значение 1, если определенный тип блокирован фильтром, и нуль в противном случае.

В качестве примера рассмотрим приложение, которое будет получать только ICMPv6-извещения маршрутизатора:

struct icmp6_filter myfilt;


fd = Socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);


ICMP6_FILTER_SETBLOCKALL(&myfilt);

ICMP6_FILTER_SETPASS(ND_ROUTER_ADVERT, &myfilt);

Setsockopt(fd, IPPROTO_ICMPV6, ICMP6_FILTER, &myfilt, sizeof(myfilt));

Сначала мы блокируем все типы сообщений (поскольку по умолчанию все типы сообщений пересылаются), а затем разрешаем пересылать только извещения маршрутизатора. Несмотря на то, что мы используем фильтр, приложение должно быть готово к получению всех типов пакетов ICMPv6, потому что любые пакеты ICMPv6, полученные между вызовами

socket
и
setsockopt
, будут добавлены в очередь на сокете. Параметр
ICMP6_FILTER
 — лишь средство оптимизации условий функционирования приложения.

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

В данном разделе приводится версия программы

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

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

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

В табл. А.5 и А.6 приведены значения поля тип (type) для этих сообщений и говорится, что значение поля код (code) равно нулю. Далее будет показано, что в поле идентификатор (identifier) указывается идентификатор процесса

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

В листинге 28.1[1] приведены примеры работы нашей программы. В первом используется версия IPv4, а во втором IPv6. Обратите внимание, что мы установили для нашей программы

ping
флаг set-user-ID (установка идентификатора пользователя при выполнении), потому что для создания символьного сокета требуются права привилегированного пользователя.

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

freebsd % ping www.google.com

PING www.google.com (216.239.57.99): 56 data bytes

64 bytes from 216.239.57.99: seq=0, ttl=53, rtt=5.611 ms

64 bytes from 216.239.57.99: seq=1, ttl=53, rtt=5.562 ms

64 bytes from 216.239.57 99: seq=2, ttl=53, rtt=5.589 ms

64 bytes from 216.239.57.99: seq=3, ttl=53, rtt=5.910 ms


freebsd % ping www.kame.net

PING orange.kame.net (2001:200:0:4819:203:47ff:fea5:3085): 56 data bytes

64 bytes from 2001:200:0:4819:203:47ff:fea5:3085: seq=0, hlim=52, rtt=422.066 ms

64 bytes from 2001:200:0:4819:203:47ff:fea5:3085: seq=1, hlim=52, rtt=417.398 ms

64 bytes from 2001:200:0:4819:203:47ff:fea5:3085: seq=2, hlim=52, rtt=416.528 ms

64 bytes from 2001:200:0:4819.203.47ff:fea5:3085: seq=3, hlim=52, rtt=429.192 ms

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

ping
.

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

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

SIGALRM
.

В листинге 28.2 приведен заголовочный файл

ping.h
, подключаемый во всех файлах программы.

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

//ping/ping.h

 1 #include "unp.h"

 2 #include 

 3 #include 

 4 #include 


 5 #define BUFSIZE 1500


 6 /* глобальные переменные */

 7 char sendbuf[BUFSIZE];


 8 int datalen; /* размер данных после заголовка ICMP */

 9 char *host;

10 int nsent; /* увеличиваем на 1 для каждого sendto() */

11 pid_t pid; /* наш PID */

12 int sockfd;

13 int verbose;


14 /* прототипы функций */

15 void init_v6(void);

16 void proc_v4(char*, ssize_t, struct msghdr*, struct timeval*);

17 void proc_v6(char*, ssize_t., struct msghdr*, struct timeval*);

18 void send_v4(void);

19 void send_v6(void):

20 void readloop(void);

21 void sig_alrm(int);

22 void tv_sub(struct timeval*, struct timeval*);


23 struct proto {

24  void (*fproc)(char*, ssize_t, struct msghdr*, struct timeval*);

25  void (*fsend)(void);

26  void (*finit)(void);

27  struct sockaddr *sasend; /* структура sockaddr{} для отправки,

                                полученная от getaddrinfo */

28  struct sockaddr *sarecv; /* sockaddr{} для получения */

29  socklen_t salen; /* длина sockaddr{} */

30  int icmpproto; /* значение IPPROTO_xxx для ICMP */

31 } *pr;


32 #ifdef IPV6

33 #include 

34 #include 


35 #endif

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

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

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

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

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

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

Функция

main
приведена в листинге 28.3.

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

//ping/main.c

 1 #include "ping.h"


 2 struct proto proto_v4 =

 3 { proc_v4, send_v4, NULL, NULL, NULL, 0, IPPROTO_ICMP };


 4 #ifdef IPV6

 5 struct proto proto_v6 =

 6 { proc_v6, send_v6, init_v6, NULL, NULL, 0, IPPROTO_ICMPV6 };

 7 #endif


 8 int datalen = 56; /* размер данных в эхо-запросе ICMP */


 9 int

10 main(int argc, char **argv)

11 {

12  int c;

13  struct addrinfo *ai;

14  char *h;


15  opterr = 0; /* отключаем запись сообщений getopt() в stderr */

16  while ((с = getopt(argc, argv, "v")) != -1) {

17   switch (c) {

18   case 'v':

19    verbose++;

20    break;


21   case '?':

22    err_quit("unrecognized option %c", c);

23   }

24  }


25  if (optind != argc-1)

26   err_quit("usage: ping [ -v ] ");

27  host = argv[optind];


28  pid = getpid() & 0xffff; /* поле идентификатора ICMP имеет размер 16 бит */

29  Signal(SIGALRM, sig_alrm);


30  ai = Host_serv(host, NULL, 0, 0);


31  h = Sock_ntop_host(ai->ai_addr, ai->ai_addrlen);

32  printf("PING %s (%s): %d data bytes\n",

33  ai->ai_canonname ? ai->ai_canonname : h, h, datalen);


34  /* инициализация в соответствии с протоколом */

35  if (ai->ai_family == AF_INET) {

36   pr = &proto_v4;

37 #ifdef IPV6

38  } else if (ai->ai_family == AF_INET6) {

39   pr = &proto_v6;

40   if (IN6_IS_ADDR_V4MAPPED(&(((struct sockaddr_in6*)

41    ai->ai_addr)->sin6_addr)))

42    err_quit("cannot ping IPv4-mapped IPv6 address");

43 #endif

44  } else

45   err_quit("unknown address family %d", ai->ai_family);


46  pr->sasend = ai->ai_addr;

47  pr->sarecv = Calloc(1, ai->ai_addrlen);

48  pr->salen = ai->ai_addrlen;


49  readloop();


50  exit(0);

51 }

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

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

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

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

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

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

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

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

Обработка ответов осуществляется функцией

readlоор
, представленной в листинге 28.4.

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

//ping/readlоор.c

 1 #include "ping.h"


 2 void

 3 readloop(void)

 4 {

 5  int size;

 6  char recvbuf[BUFSIZE];

 7  char controlbuf[BUFSIZE];

 8  struct msghdr msg;

 9  struct iovec iov;

10  ssize_t n;

11  struct timeval tval;


12  sockfd = Socket(pr->sasend->sa_family, SOCK_RAW, pr->icmpproto);

13  setuid(getuid()); /* права привилегированного пользователя

                         больше не нужны */

14  if (pr->finit)

15   (*pr->finit)();


16  size = 60 * 1024; /* setsockopt может завершиться с ошибкой */

17  setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));


18  sig_alrm(SIGALRM); /* отправка первого пакета */

19  iov.iov_base = recvbuf;

20  iov.iov_len = sizeof(recvbuf);

21  msg.msg_name = pr->sarecv;

22  msg.msg_iov = &iov;

23  msg.msg_iovlen = 1;

24  msg.msg_control = controlbuf;

25  for (;;) {

26   msg.msg_namelen = pr->salen;

27   msg.msg_controllen = sizeof(controlbuf);

28   n = recvmsg(sockfd, &msg, 0);

29   if (n < 0) {

30    if (errno == EINTR)

31     continue;

32    else

33     err_sys("recvmsg error");

24   }


35   Gettimeofday(&tval, NULL);

36   (*pr->fproc)(recvbuf, n, &msg, &tval);

37  }

38 }

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

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

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

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

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

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

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

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

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

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

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

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

В листинге 28.5 приведена функция

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

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

//lib.tv_sub.c

 1 #include "unp.h"


 2 void

 3 tv_sub(struct timeval *out, struct timeval *in)

 4 {

 5  if ((out->tv_usec -= in->tv_usec) < 0) { /* out -= in */

 6   --out->tv_sec;

 7   out->tv_usec += 1000000;

 8  }

 9  out->tv_sec -= in->tv_sec;

10 }

В листинге 28.6 приведена функция

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

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

//ping/prov_v4.c

 1 #include "ping.h"


 2 void

 3 proc_v4(char *ptr, ssize_t len, struct msghdr *msg, struct timeval *tvrecv)

 4 {

 5  int hlen1, icmplen;

 6  double rtt;

 7  struct ip *ip;

 8  struct icmp *icmp;

 9  struct timeval *tvsend;


10  ip = (struct ip*)ptr; /* начало IP-заголовка */

11  hlen1 = ip->ip_hl << 2; /* длина IP-заголовка */

12  if (ip->ip_p != IPPROTO_ICMP)

13   return; /* не ICMP */


14  icmp = (struct icmp*)(ptr + hlen1); /* начало ICMP-заголовка */

15  if ((icmplen = len - hlen1) < 8)

16   return; /* плохой пакет */


17  if (icmp->icmp_type == ICMP_ECHOREPLY) {

18   if (icmp->icmp_id != pid)

19    return; /* это не ответ на наш ECHO_REQUEST */

20   if (icmplen < 16)

21    return; /* недостаточно данных */


22  tvsend = (struct timeval*)icmp->icmp_data;

23  tv_sub(tvrecv, tvsend);

24  rtt = tvrecv->tv_sec * 1000.0 + tvrecv->tv_usec / 1000.0;


25  printf("%d bytes from %s: seq=%u, ttl=%d, rtt=%.3f ms\n",

26   icmplen, Sock_ntop_host(pr->sarecv, pr->salen),

27   icmp->icmp_seq, ip->ip_ttl, rtt);


28  } else if (verbose) {

29   printf(" %d bytes from %s: type = %d, code = %d\n",

30   icmplen, Sock_ntop_host(pr->sarecv, pr->salen),

31   icmp->icmp_type, icmp->icmp_code);

32  }

33 }

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

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

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

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

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

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

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

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

Обработка сообщений ICMPv6 управляется функцией

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

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

 1 void

 2 init_v6()

 3 {

 4 #ifdef IPV6

 5  int on = 1;


 6  if (verbose == 0) {

 7   /* установка фильтра, пропускающего только пакеты ICMP6_ECHO_REPLY. если

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

 8   struct icmp6_filter myfilt;

 9   ICMP6_FILTER_SETBLOCKALL(&myfilt);

10   ICMP6_FILTER_SETPASS(ICMP6_ECHO_REPLY, &myfilt);

11   setsockopt(sockfd, IPPROTO_IPV6, ICMP6_FILTER, &myfilt,

12    sizeof(myfilt));

13   /* игнорируем ошибку, потому что фильтр - необязательная оптимизация */

14  }


15  /* следующую ошибку тоже игнорируем; придется обойтись без вывода

       ограничения на количество транзитных узлов */

16 #ifdef IPV6_RECVHOPLIMIT

17  /* RFC 3542 */

18  setsockopt(sockfd, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, &on, sizeof(on));

19 #else

20  /* RFC 2292 */

21  setsockopt(sockfd, IPPROTO_IPV6, IPV6_HOPLIMIT, &on, sizeof(on));

22 #endif

23 #endif

24 }

Приведенная в листинге 28.8 функция

proc_v6
обрабатывает входящие пакеты.

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

//ping/proc_v6.c

 1 #include "ping.h"


 2 void

 3 proc_v6(char *ptr, ssize_t len, struct msghdr *msg, struct timeval* tvrecv)

 4 {

 5 #ifdef IPV6

 6  double rtt;

 7  struct icmp6_hdr *icmp6;

 8  struct timeval *tvsend;

 9  struct cmsghdr *cmsg;

10  int hlim;


11  icmp6 = (struct icmp6_hdr*)ptr;

12  if (len < 8)

13   return; /* плохой пакет */


14  if (icmp6->icmp6_type == ICMP6_ECHO_REPLY) {

15   if (icmp6->icmp6_id != pid)

16    return; /* это не ответ на наш ECHO_REQUEST */

17   if (len < 16)

18    return; /* недостаточно данных */


19   tvsend = (struct timeval*)(icmp6 + 1);

20   tv_sub(tvrecv, tvsend);

21   rtt = tvrecv->tv_sec * 1000.0 + tvrecv->tv_usec / 1000.0;


22   hlim = -1;

23   for (cmsg = CMSG_FIRSTHDR(msg); cmsg != NULL;

24    cmsg = CMSG_NXTHDR(msg, cmsg)) {

25    if (cmsg->cmsg_level == IPPROTO_IPV6 &&

26     cmsg->cmsg_type == IPV6_HOPLIMIT) {

27     hlim = *(u_int32_t*)CMSG_DATA(cmsg);

28     break;

29    }

30   }

31   printf("%d bytes from %s; seq=%u, hlim=",

32    len, Sock_ntop__host(pr->sarecv, pr->salen), icmp6->icmp6_seq);

33   if (hlim == -1)

34    printf("???"); /* отсутствуют вспомогательные данные */

35   else

36    printf("%d", hlim);

37   printf(", rtt=%.3f ms\n", rtt);

38  } else if (verbose) {

39   printf(" %d bytes from type = %d, code = %d\n",

40    len, Sock_ntop_host(pr->sarecv, pr->salen);

41   icmp6->icmp6, type, icmp6->icmp6_code);

42  }

43 #endif /* IPV6 */

44 }

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

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


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

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

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

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

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

Обработчиком сигнала SIGALRM является функция

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

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

//ping/sig_alrm.c

 1 #include "ping.h"


 2 void

 3 sig_alrm(int signo)

 4 {

 5  (*pr->fsend)();


 6  alarm(1);

 7  return;

 8 }

Функция

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

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

//ping/send_v4.c

 1 #include "ping.h"


 2 void

 3 send_v4(void)

 4 {

 5  int len;

 6  struct icmp *icmp;


 7  icmp = (struct icmp*)sendbuf;

 8  icmp->icmp_type = ICMP_ECHO;

 9  icmp->icmp_code = 0;

10  icmp->icmp_id = pid;

11  icmp->icmp_seq = nsent++;

12  memset(icmp->icmp_data, 0xa5, datalen); /* заполнение по шаблону */

13  Gettimeofday((struct timeval*)icmp->icmp_data, NULL);

14  len = 8 + datalen; /* контрольная сумма по заголовку и данным */

15  icmp->icmp_cksum = 0;

16  icmp->icmp_cksum = in_cksum((u_short*)icmp, len);

17  Sendto(sockfd, sendbuf, len, 0, pr->sasend, pr->salen);

18 }

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

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

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

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

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

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

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

in_cksum
, приведенная в листинге 28.11.

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

//libfree/in_cksum.c

 1 uint16_t

 2 in_cksum(uint16_t *addr, int len)

 3 {

 4  int nleft = len;

 5  uint32_t sum = 0;

 6  uint16_t *w = addr;

 7  uint16_t answer = 0;


 8  /*

 9   * Наш алгоритм прост: к 32-разрядному аккумулятору sum мы добавляем

10   * 16-разрядные слова, а затем записываем все биты переноса из старших

11   * 16 разрядов в младшие 16 разрядов.

12   */

13  while (nleft > 1) {

14   sum += *w++;

15   nleft -= 2;

16  }


17  /* при необходимости добавляем четный байт */

18  if (nleft == 1) {

19   *(unsigned char*)(&answer) = *(unsigned char*)w;

20   sum += answer;

21  }


22  /* перемещение битов переноса из старших 16 разрядов в младшие */

23  sum = (sum >> 16) + (sum & 0xffff); /* добавление старших 16 к младшим */

24  sum += (sum >> 16); /* добавление переноса */

25  answer = ~sum; /* обрезаем по 16 разрядам */

26  return(answer);

27 }

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

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

ПРИМЕЧАНИЕ

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

Последней функцией нашей программы

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

Функция

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

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

//ping/send_v6.c

 1 #include "ping.h"


 2 void

 3 send_v6()

 4 {

 5 #ifdef IPV6

 6  int len;

 7  struct icmp6_hdr *icmp6;


 8  icmp6 = (struct icmp6_hdr*)sendbuf,

 9  icmp6->icmp6_type = ICMP6_ECHO_REQUEST;

10  icmp6->icmp6_code = 0;

11  icmp6->icmp6_id = pid;

12  icmp6->icmp6_seq = nsent++;

13  memset((icmp6 + 1), 0xa5, datalen); /* заполнение по шаблону */

14  Gettimeofday((struct timeval*)(icmp6 + 1), NULL);


15  len = 8 + datalen; /* 8-байтовый заголовок ICMPv6 */


16  Sendto(sockfd, sendbuf, len, 0, pr->sasend, pr->salen);

17  /* ядро вычисляет и сохраняет контрольную сумму само */

18 #endif /* IPV6 */

19 }

28.6. Программа traceroute

В этом разделе мы приведем собственную версию программы

traceroute
. Как и в случае с программой
ping
, приведенной в предыдущем разделе, мы представляем нашу собственную, а не общедоступную версию. Это делается для того, чтобы во-первых, получить версию, поддерживающую как IPv4, так и IPv6, а во-вторых, не отвлекаться на множество параметров, не относящихся к обсуждению сетевого программирования.

Программа

traceroute
позволяет нам проследить путь IP-дейтаграмм от нашего узла до получателя. Ее действие довольно просто, а в главе 8 книги [111] оно детально описано со множеством примеров.

В версии IPv6 программа

traceroute
использует поле TTL (в версии IPv4) или поле предельного количества транзитных узлов (называемое также полем ограничения пересылок), а также два типа ICMP-сообщений. Эта программа начинает свою работу с отправки UDP-дейтаграммы получателю, причем полю TTL (ограничения пересылок) присваивается значение 1. Такая дейтаграмма вынуждает первый маршрутизатор отправить ICMP-сообщение об ошибке «Time exceeded in transit» (Превышено время передачи). Затем значение TTL увеличивается на 1 и посылается следующая UDP-дейтаграмма, которая достигает следующего маршрутизатора. Когда UDP-дейтаграмма достигает конечного получателя, необходимо заставить узел вернуть ICMP-ошибку
Port unreachable
(Порт недоступен). Для этого UDP-дейтаграмма посылается на случайный порт, который (как можно надеяться) не используется на данном узле.

Ранние версии программы

traceroute
могли устанавливать поле TTL в заголовке IPv4 только с помощью параметра сокета
IP_HDRINCL
путем построения своего собственного заголовка. Однако современные системы поддерживают параметр сокета
IP_TTL
, позволяющий определить значение TTL для исходящих дейтаграмм. (Данный параметр сокета впервые был представлен в выпуске 4.3BSD Reno.) Проще установить данный параметр сокета, чем полностью формировать IPv4-заголовок (хотя в разделе 29.7 показано, как строить собственные заголовки IPv4 и UDP). Параметр сокета IPv6
IPV6_UNICAST_HOPS
позволяет контролировать поле предельного количества транзитных узлов (ограничения пересылок) в дейтаграммах IPv6.

В листинге 28.13 приведен заголовочный файл t

race.h
, подключаемый ко всем файлам нашей программы.

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

//traceroute/trace.h

 1 #include "unp.h"

 2 #include 

 3 #include 

 4 #include 

 5 #include 


 6 #define BUFSIZE 1500


 7 struct rec { /* структура данных UDP */

 8  u_short rec_seq; /* порядковый номер */

 9  u_short rec_ttl; /* значение TTL, с которым пакет отправляется */

10  struct timeval rec_tv; /* время отправки пакета */

11 };


12 /* глобальные переменные */

13 char recvbuf[BUFSIZE];

14 char sendbuf[BUFSIZE];


15 int datalen; /* размер данных в байтах после заголовка ICMP */

16 char *host;

17 u_short sport, dport;

18 int nsent; /* добавляет 1 для каждого вызова sendto() */

19 pid_t pid; /* идентификатор нашего процесса PID */

20 int probe, nprobes;

21 int sendfd, recvfd; /* посылает на сокет UDP. читает на

                          символьном сокете ICMP */

22 int ttl, max_ttl;

23 int verbose;


24 /* прототипы функций */

25 char *icmpcode_v4(int);

26 char *icmpcode_v6(int);

27 int recv_v4(int. struct timeval*);

28 int recv_v6(int. struct timeval*);

29 void sig_alrm(int);

30 void traceloop(void);

31 void tv_sub(struct timeval*, struct timeval*);


32 struct proto {

33  char *(*icmpcode)(int);

34  int (*recv)(int. struct timeval*);

35  struct sockaddr *sasend; /* структура sockaddr{} для отправки.

                                получена из getaddrinfo */

36  struct sockaddr *sarecv; /* структура sockaddr{} для получения */

37  struct sockaddr *salast; /* последняя структура sockaddr{} для получения */

38  struct sockaddr *sabind; /* структура sockaddr{} для связывания

                                порта отправителя*/

39  socklen_t salen; /* длина структур sockaddr{}s */

40  int icmpproto; /* значение IPPROTO_xxx для ICMP */

41  int ttl level; /* значение аргумента level функции

                      setsockopt() для задания TTL */

42  int ttloptname; /* значение аргумента name функции

                       setsockopt() для задания TTL */

43 } *pr;


44 #ifdef IPV6


45 #include "ip6.h" /* должно быть  */

46 #include "icmp6.h" /* должно быть  */


47 #endif

1-11
 Подключаются стандартные заголовочные файлы IPv4, определяющие структуры и константы IPv4, ICMPv4 и UDP. Структура
rec
определяет часть посылаемой UDP-дейтаграммы, содержащую собственно данные, но, как мы увидим дальше, нам никогда не придется исследовать эти данные. Они отсылаются в основном для целей отладки.

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

32-43
Как и в программе
ping
, описанной в предыдущем разделе, мы обрабатываем различие между протоколами IPv4 и IPv6, определяя структуру
proto
, которая содержит указатели на функции, указатели на структуры адресов сокетов и другие константы, различные для двух версий IP. Глобальная переменная
pr
будет установлена как указатель на одну из этих структур, инициализированных либо для IPv4, либо для IPv6, после того как адрес получателя будет обработан функцией
main
(поскольку именно адрес получателя определяет, какая версия используется — IPv4 или IPv6).

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

44-47
Подключаются заголовочные файлы, определяющие структуры и константы IPv6 и ICMPv6.

Функция

main
приведена в листинге 28.14. Она обрабатывает аргументы командной строки, инициализирует указатель
pr
либо для IPv4, либо для IPv6 и вызывает нашу функцию
traceloop
.

Листинг 28.14. Функция main программы traceroute

//traceroute/main.c

 1 #include "trace.h"


 2 struct proto proto_v4 =

 3  {icmpcode_v4, recv_v4, NULL, NULL, NULL, NULL, 0,

 4 IPPROTO_ICMP, IPPROTO_IP, IP_TTL};


 5 #ifdef IPV6

 6 struct proto proto_v6 =

 7  {icmpcode_v6, recv_v6, NULL, NULL, NULL, NULL, 0,

 8 IPPROTO_ICMPV6, IPPROTO_IPV6, IPV6_UNICAST_HOPS};

 9 #endif


10 int datalen = sizeof(struct rec); /* значения по умолчанию */

11 int max_ttl = 30;

12 int nprobes = 3;

13 u_short dport = 32768 + 666;


14 int

15 main(int argc, char **argv)

16 {

17  int c;

18  struct addrinfo *ai;


19  opterr = 0; /* чтобы функция getopt() не записывала в stderr */

20  while ((с = getopt(argc, argv, "m:v")) != -1) {

21   switch (c) {

22   case 'm':

23    if ((max_ttl = atoi(optarg)) <= 1)

24     err_quit("invalid -m value");

25    break;


26   case 'v':

27    verbose++;

28    break;


29   case '?':

30    err_quit("unrecognized option: %c", c);

31   }

32  }


33  if (optind != argc - 1)

34   err_quit("usage: traceroute [ -m  -v ] ");

35  host = argv[optind];


36  pid = getpid();

37  Signal(SIGALRM, sig_alrm);


38  ai = Host_serv(host, NULL, 0, 0);


39  printf("traceroute to %s (%s): %d hops max, %d data bytes\n",

40   ai->ai_canonname,

41   Sock_ntop_host(ai->ai_addr, ai->ai_addrlen);

42  max_ttl, datalen);


43  /* инициализация в зависимости от протокола */

44  if (ai->ai_family == AF_INET) {

45   pr = &proto_v4;

46 #ifdef IPV6

47  } else if (ai->ai_family == AF_INET6) {

48   pr = &proto_v6;

49  if (IN6_IS_ADDR_V4MAPPED

50   (&(((struct sockaddr_in6*)ai->ai_addr)->sin6_addr)))

51   err_quit("cannot traceroute IPv4-mapped IPv6 address");

52 #endif

53  } else

54   err_quit("unknown address family %d", ai->ai_family);

55  pr->sasend = ai->ai_addr; /* содержит адрес получателя */

56  pr->sarecv = Calloc(1, ai->ai_addrlen);

57  pr->salast = Calloc(1, ai->ai_addrlen);

58  pr->sabind = Calloc(1, ai->ai_addrlen);

59  pr->salen = ai->ai_addrlen;


60  traceloop();


61  exit(0);

62 }

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

2-9
 Определяются две структуры
proto
, одна для IPv4 и другая для IPv6, хотя указатели на структуры адреса сокета не размещаются в памяти до окончания выполнения данной функции.

Установка значений по умолчанию

10-13
 Максимальное значение поля TTL или поля предельного количества транзитных узлов, используемое в программе, по умолчанию равно 30. Предусмотрен параметр командной строки
-m
, чтобы пользователь мог поменять это значение. Для каждого значения TTL посылается три пробных пакета, но их количество также может быть изменено с помощью параметра командной строки. Изначально используется номер порта получателя 32 768 + 666, и каждый раз, когда посылается новая дейтаграмма UDP, это значение увеличивается на 1. Мы можем надеяться, что порты с такими номерами не используются на узле получателя в тот момент, когда приходит дейтаграмма, однако гарантии здесь нет.

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

19-37
 Параметр командной строки -v позволяет вывести все остальные ICMP-сообщения.

Обработка имени узла или IP-адреса и завершение инициализации

38-58
 Имя узла получателя или IP-адрес обрабатывается функцией
host_serv
, возвращающей указатель на структуру
addrinfo
. В зависимости от типа возвращенного адреса (IPv4 или IPv6) заканчивается инициализация структуры
proto
, сохраняется указатель в глобальной переменной pr, а также размещается в памяти дополнительная структура адреса сокета соответствующего размера.

Функция

traceloop
, приведенная в листинге 28.15, отправляет дейтаграммы и читает вернувшиеся ICMP-сообщения. Это основной цикл программы.

Листинг 28.15. Функция traceloop: основной цикл обработки

//traceroute/traceloop.c

 1 #include "trace.h"


 2 void

 3 traceloop(void)

 4 {

 5  int seq, code, done;

 6  double rtt;

 7  struct rec *rec;

 8  struct timeval tvrecv;


 9  recvfd = Socket(pr->sasend->sa_family, SOCK_RAW, pr->icmpproto);

10  setuid(getuid()); /* права привилегированного пользователя больше

                         не нужны */


11 #ifdef IPV6

12  if (pr->sasend->sa_family == AF_INET6 && verbose == 0) {

13   struct icmp6_filter myfilt;

14   ICMP6_FILTER_SETBLOCKALL(&myfilt);

15   ICMP6_FILTER_SETPASS(ICMP6_TIME_EXCEEDED, &myfilt);

16   ICMP6_FILTER_SETPASS(ICMP6_DST_UNREACH, &myfilt);

17   setsockopt(recvfd, IPPROTO_IPV6, ICMP6_FILTER,

18    &myfilt, sizeof(myfilt));

19  }

20 #endif


21  sendfd = Socket(pr->sasend->sa_family, SOCK_DGRAM, 0);


22  pr->sabind->sa_family = pr->sasend->sa_family;

23  sport = (getpid() & 0xffff) | 0x8000; /* UDP-порт отправителя # */

24  sock_set_port(pr->sabind, pr->salen, htons(sport));

25  Bind(sendfd, pr->sabind, pr->salen);


26  sig_alrm(SIGALRM);


27  seq = 0;

28  done = 0;

29  for (ttl = 1; ttl <= max_ttl && done == 0; ttl++) {

30   Setsockopt(sendfd, pr->ttllevel, pr->ttloptname, &ttl, sizeof(int));

31   bzero(pr->salast, pr->salen);


32   printf("%2d ", ttl);

33   fflush(stdout);


34   for (probe = 0; probe < nprobes; probe++) {

35    rec = (struct rec*)sendbuf;

36    rec->rec_seq = ++seq;

37    rec->rec_ttl = ttl;

38    Gettimeofday(&rec->rec_tv, NULL);


39    sock_set_port(pr->sasend, pr->salen, htons(dport + seq));

40    Sendto(sendfd, sendbuf, datalen, 0, pr->sasend, pr->salen);

41    if ((code = (*pr->recv)(seq, &tvrecv)) == -3)

42     printf(" *"); /* тайм-аут, ответа нет */

43    else {

44     char str[NI_MAXHOST];


45     if (sock_cmp_addr(pr->sarecv, pr->salast, pr->salen) != 0) {

46      if (getnameinfo(pr->sarecv, pr->salen, str, sizeof(str),

47       NULL, 0, 0) == 0)

48       printf(" %s (%s)", str,

49        Sock_ntop_host(pr->sarecv, pr->salen));

50      else

51       printf(" %s", Sock_ntop_host(pr->sarecv, pr->salen));

52      memcpy(pr->salast, pr->sarecv, pr->salen);

53     }

54     tv_sub(&tvrecv, &rec->rec_tv);

55     rtt = tvrecv.tv_sec * 1000.0 + tvrecv.tv_usec / 1000.0;

56     printf(" %.3f ms", rtt);


57     if (code == -1) /* порт получателя недоступен */

58      done++;

59     else if (code >= 0)

60      printf(" (ICMP %s)", (*pr->icmpcode)(code));

61    }

62    fflush(stdout);

63   }

64   printf("\n");

65  }

66 }

Создание двух сокетов

9-10
 Нам необходимо два сокета: символьный сокет, на котором мы читаем все вернувшиеся ICMP-сообщения, и UDP-сокет, на который мы посылаем пробные пакеты с увеличивающимся значением поля TTL. После создания символьного сокета мы заменяем наш эффективный идентификатор пользователя на фактический, поскольку более нам не понадобятся права привилегированного пользователя.

Установка фильтра ICMPv6

11-20
 Если мы отслеживаем маршрут к адресату IPv6 и параметр командной строки -V указан не был, можно установить фильтр, который будет блокировать все ICMP-сообщения, за исключением тех, которые нас интересуют: «Time exceeded» и «Destination unreachable». Это сократит число пакетов, получаемых на данном сокете.

Связывание порта отправителя UDP-сокета

21-25
 Осуществляется связывание порта отправителя с UDP-сокетом, который используется для отправки пакетов. При этом берется 16 младших битов из идентификатора нашего процесса, а старшему биту присваивается 1. Поскольку несколько копий программы
traceroute
могут работать одновременно, нам необходима возможность определить, относится ли поступившее ICMP-сообщение к одной из наших дейтаграмм или оно пришло в ответ на дейтаграмму, посланную другой копией программы. Мы используем порт отправителя в UDP-заголовке для определения отправляющего процесса, поскольку возвращаемое ICMP-сообщение всегда содержит UDP-заголовок дейтаграммы, вызвавшей ICMP-ошибку.

Установка обработчика сигнала SIGALRM

26
 Мы устанавливаем нашу функцию
sig_alrm
в качестве обработчика сигнала
SIGALRM
, поскольку каждый раз, когда мы посылаем UDP-дейтаграмму, мы ждем 3 с, прежде чем послать следующий пробный пакет.

Основной цикл: установка TTL или предельного количества транзитных узлов и отправка трех пробных пакетов

27-38
 Основным циклом функции является двойной вложенный цикл
for
. Внешний цикл стартует со значения TTL или предельного количества транзитных узлов, равного 1, и увеличивает это значение на 1, в то время как внутренний цикл посылает три пробных пакета (UDP-дейтаграммы) получателю. Каждый раз, когда изменяется значение TTL, мы вызываем
setsockopt
для установки нового значения, используя параметр сокета
IP_TTL
или
IPV6_UNICAST_HOPS
.

Каждый раз во внешнем цикле мы инициализируем нулем структуру адреса сокета, на которую указывает

salast
. Данная структура будет сравниваться со структурой адреса сокета, возвращенной функцией
recvfrom
, при считывании ICMP-сообщения, и если эти две структуры будут различны, на экран будет выведен IP-адрес из новой структуры. При использовании этого метода для каждого значения TTL выводится IP-адрес, соответствующий первому пробному пакету, а если для данного значения TTL IP-адрес изменится (то есть во время работы программы изменится маршрут), то будет выведен новый IP-адрес.

Установка порта получателя и отправка UDP-дейтаграммы

39-40
 Каждый раз, когда посылается пробный пакет, порт получателя в структуре адреса сокета
sasend
меняется с помощью вызова функции
sock_set_port
. Причина, по которой порт меняется для каждого пробного пакета, заключается в том, что когда мы достигаем конечного получателя, все три пробных пакета посылаются на разные порты, чтобы увеличить шансы на обращение к неиспользуемому порту. Функция
sendto
посылает UDP-дейтаграмму.

Чтение ICMP-сообщения

41-42
 Одна из функций
recv_v4
или
recv_v6
вызывает функцию recvfrom для чтения и обработки вернувшихся ICMP-сообщений. Обе эти функции возвращают значение -3 в случае истечения времени ожидания (сообщая, что следует послать следующий пробный пакет, если для данного значения TTL еще не посланы все три пакета), значение -2, если приходит ICMP-ошибка о превышении времени передачи, и значение -1, если получена ICMP-ошибка «Port unreachable» (Порт недоступен), то есть достигнут конечный получатель. Если же приходит какая-либо другая ICMP-ошибка недоступности получателя («Destination unreachable»), эти функции возвращают неотрицательный ICMP-код.

Вывод ответа

43-63
 Как отмечалось выше, в случае первого ответа для данного значения TTL, а также если для данного TTL меняется IP-адрес узла, посылающего ICMP-сообщение, выводится имя узла и IP-адрес (или только IP-адрес, если вызов функции
getnameinfo
не возвращает имени узла). Время RTT вычисляется как разность между временем отправки пробного пакета и временем возвращения и вывода ICMP-сообщения.

Функция

recv_v4
приведена в листинге 28.16.

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

//traceroute/recv_v4

 1 #include "trace.h"


 2 extern int gotalarm;


 3 /* Возвращает:

 4  * -3 при тайм-ауте

 5  * -2 при сообщении ICMP time exceeded in transit (продолжаем поиск)

 6  * -1 при сообщении ICMP port unreachable (цель достигнута)

 7  * неотрицательные значения соответствуют всем прочим ошибкам ICMP

 8  */


 9 int

10 recv_v4(int seq, struct timeval *tv)

11 {

12  int hlen1, hlen2, icmplen, ret;

13  socklen_t len;

14  ssize_t n;

15  struct ip *ip, *hip;

16  struct icmp *icmp;

17  struct udphdr *udp;


18  gotalarm = 0;

19  alarm(3);

20  for (;;) {

21   if (gotalarm)

22    return(-3); /* истек таймер */

23   len = pr->salen;

24   n = recvfrom(recvfd, recvbuf, sizeof(recvbuf), 0, pr->sarecv, &len);

25   if (n < 0) {

26    if (errno == EINTR)

27     continue;

28    else

29     err_sys("recvfrom error");

30   }


31   ip = (struct ip*)recvbuf; /* начало IP-заголовка */

32   hlenl = ip->ip_hl << 2; /* длина IP-заголовка */


33   icmp = (struct icmp*)(recvbuf + hlen1); /* начало ICMP-заголовка */

34   if ((icmplen = n - hlen1) < 8)

35    continue; /* недостаточно данных для проверки ICMP-заголовка */


36   if (icmp->icmp_type == ICMP_TIMXCEED &&

37    icmp->icmp_code == ICMP_TIMXCEED_INTRANS) {

38    if (icmplen < 8 + sizeof(struct ip))

39     continue; /* недостаточно данных для проверки внутреннего IP */


40    hip = (struct ip*)(recvbuf + hlen1 + 8);

41    hlen2 = hip->ip_hl << 2;

42    if (icmplen < 8 + hlen2 + 4)

43     continue; /* недостаточно данных для проверки UDP-порта */


44    udp = (struct udphdr*)(recvbuf + hlen1 + 8 + hlen2);

45    if (hip->ip_p == IPPROTO_UDP &&

46     udp->uh_sport == htons(sport) &&

47     udp->uh_dport == htons(dport + seq)) {

48     ret = -2; /* ответил промежуточный маршрутизатор */

49     break;

50    }


51   } else if (icmp->icmp_type == ICMP_UNREACH) {

52    if (icmplen < 8 + sizeof(struct ip))

53     continue; /* недостаточно данных для проверки внутреннего IP */


54    hip = (struct ip*)(recvbuf + hlen1 + 8);

55    hlen2 = hip->ip_hl << 2;

56    if (icmplen < 8 + hlen2 + 4)

57     continue; /* недостаточно данных для проверки UDP-портов */


58    udp = (struct udphdr*)(recvbuf + hlen1 + 8 + hlen2);

59    if (hip->ip_p == IPPROTO_UDP &&

60     udp->uh_sport == htons(sport) &&

61     udp->uh_dport == htons(dport + seq)) {

62     if (icmp->icmp_code == ICMP_UNREACH_PORT)

63      ret = -1; /* цель достигнута */

64     else

65      ret = icmp->icmp_code; /* 0, 1, 2, ... */

66     break;

67    }

68   }

69   if (verbose) {

70    printf(" (from %s: type = %d, code - %d)\n",

71     Sock_ntop_host(pr->sarecv, pr->salen),

72     icmp->icmp_type, icmp->icmp_code);

73   }

74   /* другая ICMP-ошибка, нужно снова вызвать recvfrom() */

75  }

76  alarm(0); /* отключаем таймер */

77  Gettimeofday(tv, NULL); /* время получения пакета */

78  return(ret);

79 }

Установка таймера и прочтение каждого ICMP-сообщения

17-27
 Таймер устанавливается на 3 с, и функция входит в цикл, вызывающий
recvfrom
, считывая каждое ICMPv4-сообщение, возвращаемое на символьный сокет.

ПРИМЕЧАНИЕ

Эта функция не создает ситуации гонок, описанной в разделе 20.5, благодаря использованию глобального флага.

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

31-35
 Указатель
указывает на начало IPv4-заголовка (напомним, что операция чтения на символьном сокете всегда возвращает IP-заголовок), а указатель
icmp
указывает на начало ICMP-заголовка. На рис. 28.5 показаны различные заголовки, указатели и длины, используемые в данном коде.

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

Обработка ICMP-сообщения о превышении времени передачи

36-50
 Если ICMP-сообщение является сообщением «Time exceeded in transit» (Превышено время передачи), вероятно, оно является ответом на один из наших пробных пакетов. Указатель
hip
указывает на заголовок IPv4, который возвращается в ICMP-сообщении и следует сразу за 8-байтовым ICMP-заголовком. Указатель
udp
указывает на следующий далее UDP-заголовок. Если ICMP-сообщение было сгенерировано UDP-дейтаграммой и если порты отправителя и получателя этой дейтаграммы совпадают с теми значениями, которые мы посылали, то тогда это ответ от промежуточного маршрутизатора на наш пробный пакет.

Обработка ICMP-сообщения о недоступности порта

51-68
 Если ICMP-сообщение является сообщением «Destination unreachable» (Получатель недоступен), тогда, чтобы узнать, является ли это сообщение ответом на наш пробный пакет, мы смотрим на UDP-заголовок, возвращенный в данном ICMP-сообщении. Если это так и код означает сообщение «Port unreachable» (Порт недоступен), то возвращается значение -1, поскольку достигнут конечный получатель. Если же ICMP-сообщение является ответом на один из наших пробных пакетов, но не является сообщением типа «Destination unreachable» (Получатель недоступен), то тогда возвращается значение ICMP-кода. Обычным примером такого случая является ситуация, когда брандмауэр возвращает какой-либо другой код недоступности для получателя, на который посылается пробный пакет.

Обработка других ICMP-сообщений

69-73
Все остальные ICMP-сообщения выводятся, если был задан параметр
-v
.

Следующая функция, recv_v6, приведена в листинге 28.18 и является IPv6-вepсией ранее описанной функции для IPv4. Эта функция почти идентична функции

recv_v4
, за исключением различий в именах констант и элементов структур. Кроме того, размер заголовка IPv6 является фиксированным и составляет 40 байт, в то время как для получения IP-параметров в заголовке IPv4 необходимо получить поле длины заголовка и умножить его на 4. На рис. 28.6 приведены различные заголовки, указатели и длины, используемые в коде.

Рис. 28.6. Заголовки, указатели и длины, используемые при обработке ошибки ICMPv6

Мы определяем две функции

icmpcode_v4
и
icmpcode_v6
, которые можно вызывать в конце функции
traceloop
для вывода строки описания, соответствующей ICMP-ошибке недоступности получателя. В листинге 28.19 приведена IPv6-функция. IPv4-функция аналогична, хотя и длиннее, поскольку существует большее количество ICMPv4-кодов недоступности получателя (см. табл. А.5).

Последней функцией в нашей программе

traceroute
является обработчик сигнала
SIGALRM
— функция
sig_alrm
, приведенная в листинге 28.17. Эта функция лишь возвращает ошибку
EINTR
из функции
recvfrom
, как в случае функции
recv_v4
, так и в случае
recv_v6
.

Листинг 28.17. Функция sig_alrm

//traceroutе/sig_alrm.c

1 #include "trace.h"


2 int gotalarm;

3 void

4 sig_alrm(int signo)

5 {

6  gotalarm = 1; /* установка флага, оповещающего о сигнале */

7  return; /* прерывается работа функции recvfrom() */

8 }

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

//traceroute/recv_v6

 1 #include "trace.h"


 2 extern int gotalarm;


 3 /*

 4  * Возвращает; -3 при тайм-ауте

 5  * -2 для сообщения ICMP time exceeded in transit (продолжаем поиск

      маршрута)

 6  * -1 для сообщения ICMP port unreachable (цель достигнута)

 7  * неотрицательные значения соответствуют всем прочим ICMP-сообщениям

 8  */


 9 int

10 recv_v6(int seq, struct timeval *tv)

11 {

12 #ifdef IPV6

13  int hlen2, icmp6len, ret;

14  ssize_t n;

15  socklen_t len;

16  struct ip6_hdr *hip6;

17  struct icmp6_hdr *icmp6;

18  struct udphdr *udp;


19  gotalarm = 0;

20  alarm(3);

21  for (;;) {

22   if (gotalarm)

23    return(-3); /* истек таймер */

24   len = pr->salen;

25   n = recvfrom(recvfd, recvbuf, sizeof(recvbuf), 0, pr->sarecv, &len);

26   if (n < 0) {

27    if (errno == EINTR)

28     continue;

29    else

30     err_sys("recvfrom error");

31   }


32   icmp6 = (struct icmp6_hdr*)recvbuf; /* ICMP-заголовок */

33   if ((icmp6len = n) < 8)

34    continue; /* недостаточно для проверки ICMP-заголовка */


35   if (icmp6->icmp6_type == ICMP6_TIME_EXCEEDED &&

36    icmp6->icmp6_code == ICMP6_TIME_EXCEED_TRANSIT) {

37    if (icmp6len < 8 + sizeof(struct ip6_hdr) + 4)

38     continue; /* недостаточно для проверки внутреннего заголовка */


39    hip6 = (struct ip6_hdr*)(recvbuf + 8);

40    hlen2 = sizeof(struct ip6_hdr);

41    udp = (struct udphdr*)(recvbuf + 8 + hlen2);

42    if (hip6->ip6_nxt == IPPROTO_UDP &&

43     udp->uh_sport == htons(sport) &&

44     udp->uh_dport == htons(dport + seq))

45     ret = -2; /* ответил промежуточный маршрутизатор */

46    break;


47   } else if (icmp6->icmp6_type == ICMP6_DST_UNREACH) {

48    if (icmp6len < 8 + sizeof(struct ip6_hdr) + 4)

49     continue; /* недостаточно для проверки внутреннего заголовка */


50    hip6 = (struct ip6_hdr*)(recvbuf + 8);

51    hlen2 = sizeof(struct ip6_hdr);

52    udp = (struct udphdr*)(recvbuf + 8 + hlen2);

53    if (hip6->ip6_nxt == IPPROTO_UDP &&

54     udp->uh_sport == htons(sport) &&

55     udp->uh_dport == htons(dport + seq)) {

56     if (icmp6->icmp6_code == ICMP6_DST_UNREACH_NOPORT)

57      ret = -1; /* цель достигнута */

58     else

59      ret = icmp6->icmp6_code; /* 0, 1, 2, ... */

60     break;

61    }

62   } else if (verbose) {

63    printf(" (from %s: type = %d, code = %d)\n",

64     Sock_ntop_host(pr->sarecv, pr->salen);

65    icmp6->icmp6_type, icmp6->icmp6_code);

66   }

67   /* другая ICMP-ошибка. нужно вызвать recvfrom() */

68  }

69  alarm(0); /* отключаем таймер */

70  Gettimeofday(tv, NULL); /* get time of packet arrival */

71  return(ret);

72 #endif

73 }

Листинг 28.19. Возвращение строки, соответствующей коду недоступности ICMPv6

//traceroute/icmpcode_v6.c

 1 #include "trace.h"


 2 const char *

 3 icmpcode_v6(int code)

 4 {

 5 #ifdef IPV6

 6  static char errbuf[100];

 7  switch (code) {

 8  case ICMP6_DST_UNREACH_NOROUTE:

 9   return("no route to host");

10  case ICMP6_DST_UNREACH_ADMIN:

11   return("administratively prohibited");

12  case ICMP6_DST_UNREACH_NOTNEIGHBOR:

13   return("not a neighbor");

14  case ICMP6_DST_UNREACH_ADDR:

15   return("address unreachable");

15  case ICMP6_DST_UNREACH_NOPORT:

16   return("port unreachable");

17  default:

18   sprintf(errbuf, "[unknown code %d]",. code);

19   return errbuf;

20  }

21 #endif

22 }

Пример

Сначала приведем пример с Ipv4:

freebsd % traceroute www.unpbook.com

traceroute to www.unpbook.com (206.168.112.219): 30 hops max. 24 data bytes

1 12.106.32.1 (12.106.32.1) 0.799 ms 0.719 ms 0.540 ms

2 12.124.47.113 (12.124.47.113) 1.758 ms 1.760 ms 1.839 ms

3 gbr2-p27.sffca.ip.att.net (12.123.195.38) 2.744 ms 2.575 ms 2.648 ms

4 tbr2-p012701.sffca.ip.att.net (12.122.11.85) 3.770 ms 3.689 ms 3.848 ms

5 gbr3-p50.dvmco.ip.att.net (12.122.2.66) 26.202 ms 26.242 ms 26.102 ms

6 gbr2-p20.dvmco.ip.att.net (12.122.5.26) 26 255 ms 26.194 ms 26.470 ms

7 gar2-p370.dvmco.ip.att.net (12.123.36.141) 26.443 ms 26.310 ms 26.427 ms

8 att-46.den.internap.ip.att.net (12.124.158.58) 26.962 ms 27.130 ms 27.279 ms

9 border10 ge3-0-bbnet2.den.pnap.net (216.52.40.79) 27.285 ms 27 293 ms 26.860 ms

10 coop-2.border10.den.pnap.net (216 52.42.118) 28.721 ms 28.991 ms 30.077 ms

11 199.45.130.33 (199.45.130.33) 29.095 ms 29.055 ms 29 378 ms

12 border-to-141-netrack.boulder.со.coop.net (207.174.144.178) 30.875 ms 29.747 ms 30.142 ms

13 linux.unpbook.com (206.168.112.219) 31.713 ms 31.573 ms 33.952 ms

Ниже приведен пример с IPv6. Для лучшей читаемости длинные строки разбиты.

freebsd % traceroute www.kame.net

traceroute to orange.kame.net (2001:200:0:4819:203:47ff:fea5:3085): 30 hops max, 24 data bytes

1 3ffe:b80:3:9ad1::1 (3ffe:b80:3:9ad1::1) 107.437 ms 99.341 ms 103.477 ms

2 Viagenie-gw.int.ipv6.ascc.net (2001:288:3b0::55)

  105.129 ms 89.418 ms 90.016 ms

3 gw-Viagenie.int.ipv6.ascc.net (2001:288:3b0::54)

  302.300 ms 291.580 ms 289.839 ms

4 c7513-gw.int.ipv6.ascc.net (2001:288:3b0::c)

  296.088 ms 298.600 ms 292.196 ms

5 m160-c7513.int.ipv6.ascc.net (2001:288:3b0::1e)

  296.266 ms 314.878 ms 302.429 ms

6 m20jp-ml60tw.int.ipv6.ascc.net (2001:288:3b0::1b)

  327.637 ms 326.897 ms 347.062 ms

7 hitachi1.otemachi.wide.ad.jp (2001:200:0:1800::9c4:2)

  420.140 ms 426.592 ms 422.756 ms

8 pc3.yagami.wide.ad.jp (2001:200:0:1c04::1000:2000)

  415.471 ms 418.308 ms 461.654 ms

9 gr2000.k2c.wide.ad.jp (2001:200:0:8002::2000:1)

  416.581 ms 422.430 ms 427.692 ms

10 2001:200:0:4819:203:47ff:fea5:3085 (2001:200:0:4819:203:47ff:fea5:3085)

  417.169 ms 434.674 ms 424.037 ms

28.7. Демон сообщений ICMP

Получение асинхронных ошибок ICMP на сокет UDP всегда было и продолжает оставаться проблемой. Ядро получает сообщения об ошибках ICMP, но они редко доставляются приложениям, которым необходимо о них знать. Мы видели, что для получения этих ошибок в API сокетов требуется присоединение сокета UDP к одному IP-адресу (см. раздел 8.11). Причина такого ограничения заключается в том, что единственная ошибка, возвращаемая функцией

recvfrom
, является целочисленным кодом
errno
, а если приложение посылает дейтаграммы по нескольким адресам, а затем вызывает
recvfrom
, то данная функция не может сообщить приложению, какая из дейтаграмм вызвала ошибку.

В данном разделе предлагается решение, не требующее никаких изменений в ядре. Мы предлагаем демон ICMP-сообщений

icmpd
, который создает символьный сокет ICMPv4 и символьный сокет ICMPv6 и получает все ICMP-сообщения, направляемые к ним ядром. Он также создает потоковый сокет домена Unix, связывает его (при помощи функции
bind
) с полным именем
/tmp/icmpd
и прослушивает входящие соединения (устанавливаемые при помощи функции
connect
) клиентов с этим сокетом. Схема соединений изображена на рис. 28.7.

Рис. 28.7. Демон icmpd: создание сокетов

Приложение UDP (являющееся клиентом для демона) сначала создает сокет UDP, для которого оно хочет получать асинхронные ошибки. Приложение должно связать (функция

bind
) с этим сокетом динамически назначаемый порт; для чего это делается, будет пояснено далее. Затем оно создает доменный сокет Unix и присоединяется (функция
connect
) к заранее известному полному имени файла демона. Это показано на рис. 28.8.

Рис. 28.8. Приложение создает свой сокет UDP и доменный сокет Unix

Далее приложение «передает» свой UDP-сокет демону через соединение домена Unix, используя технологию передачи дескрипторов, как показано в разделе 15.7. Такой подход позволяет демону получить копию сокета, так что он может вызвать функцию

getsockname
и получить номер порта, связанный с сокетом. На рис. 28.9 показана передача сокета.

Рис. 28.9. Пересылка сокета UDP демону через доменный сокет Unix

После того как демон получает номер порта, связанный с UDP-сокетом, он закрывает свою копию сокета, и мы возвращаемся к схеме, приведенной на рис. 28.8.

ПРИМЕЧАНИЕ

Если узел поддерживает передачу данных, идентифицирующих отправителя (см. раздел 15.8), приложение также может послать эти данные демону. Затем демон может проверить, можно ли допускать данного пользователя к данному устройству.

В таком случае в результате любой ошибки ICMP, полученной демоном в ответ на UDP-дейтаграмму, посланную с порта, который связан с UDP-сокетом приложения, демон посылает приложению сообщение (о котором мы рассказываем чуть ниже) через доменный сокет Unix. Тогда приложение должно использовать функцию

select
или
poll
, чтобы обеспечить ожидание прибытия данных либо на UDP-сокет, либо на доменный сокет Unix.

Сначала рассмотрим исходный код приложения, использующего данный демон, а затем и сам демон. В листинге 28.20 приведен заголовочный файл, подключаемый и к приложению, и к демону.

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

//icmpd/unpicmpd.h

 1 #ifndef __unpicmp_h

 2 #define __unpicmp_h


 3 #include "unp.h"


 4 #define ICMPD_PATH "/tmp/icmpd" /* известное имя сервера */


 5 struct icmpd_err {

 6 int icmpd_errno; /* EHOSTUNREACH, EMSGSIZE, ECONNREFUSED */

 7 char icmpd_type; /* фактический тип ICMPv[46] */

 8 char icmpd_code; /* фактический код ICMPv[46] */

 9 socklen_t icmpd_len; /* длина последующей структуры sockaddr{} */

10 struct sockaddr_storage icmpd_dest; /* универсальная структура

                                          sockaddr_storage */

11 };


12 #endif /* __unpicmp_h */

4-11
 Определяются известное полное имя сервера и структура
icmpd_err
, передаваемая от сервера приложению сразу, как только получено ICMP-сообщение, которое должно быть передано данному приложению.

6-8
 Проблема в том, что типы сообщений ICMPv4 отличаются численно (а иногда и концептуально) от типов сообщений ICMPv6 (см. табл. А.5 и А.6). Возвращаются реальные значения типа (type) и кода (code), но мы также отображаем соответствующие им значения
errno
(
icmpd_errno
), взятые из последнего столбца табл. А.5 и А.6. Приложение может использовать эти значения вместо зависящих от протокола значений ICMPv4 и ICMPv6. В табл. 28.1 показаны обрабатываемые сообщения ICMP и соответствующие им значения
errno
.


Таблица 28.1. Значения переменной icmpd_errno, сопоставляющей ошибки ICMPv4 и ICMPv6

icmpd_errnoОшибка ICMPv4Ошибка ICMPv6
ECONNREFUSEDPort unreachable (Порт недоступен)Port unreachable (Порт недоступен)
EMSGSIZEFragmentation needed but DF bit set (Необходима фрагментация, но установлен бит DF)Packet too big (Слишком большой пакет)
EHOSTUNREACHTime exceeded (Превышено время передачи)Time exceeded (Превышено время передачи)
EHOSTUNREACHSource quench (Отключение отправителя)
EHOSTUNREACHВсе другие сообщения о недоступности получателя (Destination unreachable)Все другие сообщения о недоступности получателя (Destination unreachable)

Демон возвращает пять типов ошибок ICMP:

1. «Port unreachable» (Порт недоступен) означает, что сокет не связан с портом получателя на IP-адресе получателя.

2. «Packet too big» (Слишком большой пакет) используется при определении транспортной MTU. В настоящее время нет определенного API, позволяющего UDP-приложениям осуществлять определение транспортной MTU. Если ядро поддерживает определение транспортной MTU для UDP, то обычно получение данной ошибки ICMP заставляет ядро записать новое значение транспортной MTU в таблицу маршрутизации ядра, но UDP-приложение, пославшее дейтаграмму, не извещается. Вместо этого приложение должно дождаться истечения тайм-аута и повторно послать дейтаграмму, и тогда ядро найдет новое (меньшее) значение MTU в своей таблице маршрутизации и фрагментирует дейтаграмму. Передача этой ошибки приложению позволяет ему ускорить повторную передачу дейтаграммы, и возможно, приложение сможет уменьшить размер посылаемой дейтаграммы.

3. Ошибка «Time exceeded» (Превышено время передачи) обычно возникает с кодом 0 и означает, что либо значение поля TTL (в случае IPv4), либо предельное количество транзитных узлов (в случае IPv6) достигло нуля. Обычно это свидетельствует о зацикливании маршрута, что, возможно, является временной ошибкой.

4. Ошибка «Source quench» (Отключение отправителя) ICMPv4 хотя и рассматривается в RFC 1812 [6] как устаревшая, может быть послана маршрутизаторами (или неправильно сконфигурированными узлами, действующими как маршрутизаторы). Такие ошибки означают, что пакет отброшен, и поэтому обрабатываются как ошибки недоступности получателя. Следует отметить, что в версии IPv6 нет ошибки отключения отправителя.

5. Все остальные ошибки недоступности получателя (Destination unreachble) означают, что пакет сброшен.

10
 Элемент
icmpd_dest
является структурой адреса сокета, содержащей IP-адрес получателя и порта дейтаграммы, сгенерировавшей ICMP-ошибку. Этот элемент может быть структурой
sockaddr_in
для ICMPv4 либо структурой
sockaddr_in6
для ICMPv6. Если приложение посылает дейтаграммы по нескольким адресам, оно, вероятно, имеет по одной структуре адреса сокета на каждый адрес. Возвращая эту информацию в структуре адреса сокета, приложение может сравнить ее со своими собственными структурами для поиска той, которая вызвала ошибку. Тип
sockaddr_storage
используется для того, чтобы в структуре можно было хранить адреса любого типа, поддерживаемого системой.

Эхо-клиент UDP, использующий демон icmpd

Теперь модифицируем наш эхо-клиент UDP (функцию

dg_cli
) для использования нашего демона
icmpd
. В листинге 28.21 приведена первая половина функции.

Листинг 28.21. Первая часть приложения dg_cli

//icmpd/dgcli01.c

 1 #include "unpicmpd.h"


 2 void

 3 dg_cli(FILE *fp, int sockfd, const SA *pservadd, socklen_t servlen)

 4 {

 5  int icmpfd, maxfdp1;

 6  char sendline[MAXLINE], recvline[MAXLINE + 1];

 7  fd_set rset;

 8  ssize_t n;

 9  struct timeval tv;

10  struct icmpd_err icmpd_err;

11  struct sockaddr_un sun;


12  Sock_bind_wild(sockfd, pservaddr->sa_family);


13  icmpfd = Socket(AF_LOCAL, SOCK_STREAM, 0);

14  sun.sun_family = AF_LOCAL;

15  strcpy(sun.sun_path, ICMPD_PATH);

16  Connect(icmpfd, (SA*)&sun, sizeof(sun));

17  Write_fd(icmpfd, "1", 1, sockfd);

18  n = Read(icmpfd, recvline, 1);

19  if (n != 1 || recvline[0] != '1')

20   err_quit("error creating icmp socket, n = %d, char = %c",

21   n, recvline[0]);


22  FD_ZERO(&rset);

23  maxfdp1 = max(sockfd, icmpfd) + 1;

2-3
 Аргументы функции те же, что и во всех ее предыдущих версиях.

Связывание с универсальным адресом и динамически назначаемым портом

12
 Вызываем функцию
sock_bind_wild
для связывания при помощи функции
bind
универсального IP-адреса и динамически назначаемого порта с UDP-сокетом. Таким образом копия сокета, который пересылается демону, оказывается связана с портом, поскольку демону необходимо знать этот порт.

ПРИМЕЧАНИЕ

Демон также может произвести подобное связывание, если локальный порт не был связан с сокетом, который был передан демону, но это работает не во всех системах. В реализациях SVR4, таких как Solaris 2.5, сокеты не являются частью ядра, и когда один процесс связывает (bind) порт с совместно используемым сокетом, другой процесс при попытке использовать копию этого сокета получает ошибки. Простейшее решение — потребовать, чтобы приложение связывало локальный порт прежде, чем передавать сокет демону.

Установление доменного соединения Unix с демоном

13-16
 Мы создаем сокет семейства
AF_INET
и подключаемся к известному имени сервера при помощи вызова
connect
.

Отправка UDP-сокета демону, ожидание ответа от демона

17-21
 Вызываем функцию
write_fd
, приведенную в листинге 15.11 для отправки копии UDP-сокета демону. Мы также посылаем одиночный байт данных — символ
"1"
, поскольку некоторые реализации не передают дескриптор без данных. Демон посылает обратно одиночный байт данных, состоящий из символа
"1"
, для обозначения успешного выполнения. Любой другой ответ означает ошибку.

22-23
 Инициализируем набор дескрипторов и вычисляем первый аргумент для функции
select
(максимальный из двух дескрипторов, увеличенный на единицу).

Вторая половина нашего клиента приведена в листинге 28.22. Это цикл, который считывает данные из стандартного ввода, посылает строку серверу, считывает ответ сервера и записывает ответ в стандартный вывод.

Листинг 28.22. Вторая часть приложения dg_cli

//icmpd/dgcli01.c

24  while (Fgets(sendline, MAXLINE, fp) != NULL) {

25   Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);


26   tv.tv_sec = 5;

27   tv.tv_usec = 0;

28   FD_SET(sockfd, &rset);

29   FD_SET(icmpfd, &rset);

30   if ((n = Select(maxfdp1, &rset, NULL, NULL, &tv)) == 0) {

31    fprintf(stderr, "socket timeout\n");

32    continue;

33   }


34   if (FD_ISSET(sockfd, &rset)) {

35    n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);

36    recvline[n] = 0; /* завершающий нуль */

37    Fputs(recvline, stdout);

38   }


39   if (FD_ISSET(icmpfd, &rset)) {

40    if ((n = Read(icmpfd, &icmpd_err, sizeof(icmpd_err))) == 0)

41     err_quit("ICMP daemon terminated");

42    else if (n != sizeof(icmpd_err))

43     err_quit("n = %d, expected %d", n, sizeof(icmpd_err)),

44    printf("ICMP error: dest = %s, %s, type = %d, code = %d\n",

45     Sock_ntop(&icmpd_err.icmpd_dest, icmpd_err.icmpd_len);

46    strerror(icmpd_err.icmpd_errno),

47     icmpd_err.icmpd_type, icmpd_err.icmpd_code);

48   }

49  }

50 }

Вызов функции select

26-33
 Поскольку мы вызываем функцию
select
, мы можем легко установить время ожидания ответа от эхо-сервера. Задаем его равным 5 с, открываем оба дескриптора для чтения и вызываем функцию
select
. Если происходит превышение времени, выводится соответствующее сообщение и осуществляется переход в начало цикла.

Вывод ответа сервера

34-38
 Если дейтаграмма возвращается сервером, она выводится в стандартный поток вывода.

Обработка ICMP-ошибки

39-48
 Если наше доменное соединение Unix с демоном
icmpd
готово для чтения, мы пытаемся прочитать структуру
icmpd_err
. Если это удается, выводится соответствующая информация, возвращаемая демоном.

ПРИМЕЧАНИЕ

Функция strerror является примером простой, почти тривиальной функции, которая должна быть более переносимой, чем она есть. В ANSI С ничего не говорится об ошибках, возвращаемых этой функцией. В руководстве по операционной системе Solaris 2.5 говорится, что функция возвращает пустой указатель, если ее аргумент выходит за пределы допустимых значений. Это означает, что код наподобие следующего:

printf("%s", strerror(arg));

является некорректным, поскольку strerror может вернуть пустой указатель. Однако реализации FreeBSD, так же как и все реализации исходного кода, которые автор смог найти, обрабатывают неправильный аргумент, возвращая указатель на строку типа «Неизвестная ошибка». Это имеет смысл и означает, что приведенный выше код правильный. POSIX изменил ситуацию, утверждая, что поскольку не предусмотрено значение, сигнализирующее об ошибке, связанной с выходом аргумента за допустимые пределы, функция присваивает переменной errno значение EIVAL. (Ничего не сказано об указателе, возвращаемом в случае ошибки.) Это означает, что полностью правильный код должен обнулить errno, вызвать функцию strerror, проверить, не равняется ли значение errno величине EINVAL, и в случае ошибки вывести некоторое сообщение.

Примеры эхо-клиента UDP

Приведем несколько примеров работы данного клиента, прежде чем рассматривать исходный код демона. Сначала посылаем дейтаграмму на IP-адрес, не связанный с Интернетом:

freebsd % udpcli01 192.0.2.5 echo

hi there

socket timeout

and hello

socket timeout

Мы считаем, что демон

icmpd
запущен, и ждем возвращения каким-либо маршрутизатором ICMP-ошибок недоступности получателя. Вместо этого наше приложение завершается по превышению времени ожидания. Мы показываем это, чтобы повторить, что время ожидания все еще необходимо, а генерация ICMP- сообщения о недоступности узла может и не произойти.

В следующем примере дейтаграмма отправляется на порт стандартного эхо- сервера узла, на котором этот сервер не запущен. Мы получаем ожидаемое ICMPv4-сообщение о недоступности порта.

freebsd % udpcli01 aix-4 echo

hello

ICMP error: dest = 192.168.42.2:7. Connection refused, type = 3, code = 1

Выполнив ту же попытку с протоколом IPv6, мы получаем ICMPv6-сообщение о недоступности порта.

freebsd % udpcli01 aix-6 echo hello, world

ICMP error: dest = [3ffe:b80:1f8d:2:204:acff:fe17:bf38]:7. Connection refused, type = 1. code = 4

Демон icmpd

Начинаем описание нашего демона

icmpd
с заголовочного файла
icmpd.h
, приведенного в листинге 28.23.

Листинг 28.23. Заголовочный файл icmpd.h для демона icmpd

//icmpd/icmpd.h

 1 #include "unpicmpd.h"


 2 struct client {

 3  int connfd; /* потоковый доменный сокет Unix к клиенту */

 4  int family; /* AF_INET или AF_INET6 */

 5  int lport;  /* локальный порт, связанный с UDP-сокетом клиента */

 6              /* сетевой порядок байтов */

 7 } client[FD_SETSIZE];


 8 /* глобальные переменные */

 9 int fd4, fd6, listenfd, maxi, maxfd, nready;

10 fd_set rset, allset;

11 struct sockaddr_un cliaddr;


12 /* прототипы функций */

13 int readable_conn(int);

14 int readable_listen(void);

15 int readable_v4(void);

16 int readable_v6(void);

Массив client

2-17
 Поскольку демон может обрабатывать любое количество клиентов, для сохранения информации о каждом клиенте используется массив структур
client
. Они аналогичны структурам данных, которые использовались в разделе 6.8. Кроме дескриптора для доменного сокета Unix, через который осуществляется связь с клиентом, сохраняется также семейство адресов клиентского UDP-сокета
AF_INET
или
AF_INET6
и номер порта, связанного с сокетом. Далее объявляются прототипы функций и глобальные переменные, совместно используемые этими функциями.

В листинге 28.24 приведена первая часть функции main.

Листинг 28.24. Первая часть функции main: создание сокетов

//icmpd/icmpd.c

 1 #include "icmpd.h"


 2 int

 3 main(int argc, char **argv)

 4 {

 5  int i, sockfd;

 6  struct sockaddr_un sun;


 7  if (argc != 1)

 8   err_quit("usage: icmpd");


 9  maxi = -1; /* индекс массива client[] */

10  for (i = 0; i < FD_SETSIZE; i++)

11   client[i].connfd = -1; /* -1 означает свободный элемент */

12  FD_ZERO(&allset);


13  fd4 = Socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);

14  FD_SET(fd4, &allset);

15  maxfd = fd4;


16 #ifdef IPV6

17  fd6 = Socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);

18  FD_SET(fd6, &allset);

19  maxfd = max(maxfd, fd6);

20 #endif


21  listenfd = Socket(AF_UNIX, SOCK_STREAM, 0);

22  sun.sun_family = AF_LOCAL;

23  strcpy(sun.sun_path, ICMPD_PATH);

24  unlink(ICMPD_PATH);

25  Bind(listenfd, (SA*)&sun, sizeof(sun));

26  Listen(listenfd, LISTENQ);

27  FD_SET(listenfd, &allset);

28  maxfd = max(maxfd, listenfd);

Инициализация массива client

9-10
 Инициализируется массив
client
путем присваивания значения -1 элементу присоединенного сокета.

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

12-28
 Создаются три сокета: символьный сокет ICMPv4, символьный сокет ICMPv6 и потоковый доменный сокет Unix. Мы связываем при помощи функции
bind
свое заранее известное полное имя с сокетом и вызываем функцию
listen
. Это сокет, к которому клиенты присоединяются с помощью функции
connect
. Для функции
select
также вычисляется максимальный дескриптор, а для вызовов функции
accept
в памяти размещается структура адреса сокета.

В листинге 28.25 приведена вторая часть функции

main
. Она содержит бесконечный цикл, вызывающий функцию
select
в ожидании, когда будет готов к чтению какой-либо из дескрипторов демона.

Листинг 28.25. Вторая часть функции main: обработка готового к чтению дескриптора

//icmpd/icmpd.c

29  for (;;) {

30   rset = allset;

31   nready = Select(maxfd+1, &rset, NULL, NULL, NULL);


32   if (FD_ISSET(listenfd, &rset))

33    if (readable_listen() <= 0)

34     continue;


35   if (FD_ISSET(fd4, &rset))

36    if (readable_v4() <= 0)

37     continue;


38 #ifdef IPV6

39   if (FD_ISSET(fd6, &rset))

40    if (readable_v6() <= 0)

41     continue;

42 #endif


43   for (i = 0; i <= maxi; i++) { /* проверка всех клиентов */

44    if ( (sockfd = client[i].connfd) < 0)

45     continue;

46    if (FD_ISSET(sockfd, &rset))

47     if (readable_conn(i) <= 0)

48      break; /* готовых дескрипторов больше нет */

49   }

50  }

51  exit(0);

52 }

Проверка прослушиваемого доменного сокета Unix

32-34
 Прослушиваемый доменный сокет Unix проверяется в первую очередь, и если он готов, запускается функция
readable_listen
. Переменная
nready
— количество дескрипторов, которое функция select возвращает как готовые к чтению — является глобальной. Каждая из наших функций
readablе_XXX
уменьшает ее значение на 1, и новое значение этой переменной является возвращаемым значением функции. Когда ее значение достигает нуля, это говорит о том, что все готовые к чтению дескрипторы обработаны, и поэтому функция
select
вызывается снова.

Проверка символьных сокетов ICMP

35-42
 Проверяется символьный сокет ICMPv4, а затем символьный сокет ICMPv6.

Проверка присоединенных доменных сокетов Unix

43-49
 Затем проверяется, готов ли для чтения какой-нибудь из присоединенных доменных сокетов Unix. Готовность для чтения какого-либо из таких сокетов обозначает, что клиент отослал дескриптор или завершился.

В листинге 28.26 приведена функция

readable_listen
, вызываемая, когда прослушиваемый сокет готов для чтения. Это указывает на новое клиентское соединение.

Листинг 28.26. Обработка нового соединения клиента

//icmpd/readablе_listen.c

 1 #include "icmpd.h"


 2 int

 3 readable_listen(void)

 4 {

 5  int i, connfd;

 6  socklen_t clilen;


 7  clilen = sizeof(cliaddr);

 8  connfd = Accept(listenfd, (SA*)&cliaddr, &clilen);

 9  /* поиск первой свободной структуры в массиве client[] */

10  for (i = 0; i < FD_SETSIZE; i++)

11   if (client[i].connfd < 0) {

12    client[i].connfd = connfd; /* сохранение дескриптора */

13    break;

14   }

15  if (i == FD_SETSIZE) {

16   close(connfd); /* невозможно обработать новый клиент */

17   return(--nready); /* грубое закрытие нового соединения */

18  }

19  printf("new connection, i = %d, connfd = %d\n", i, connfd);


20  FD_SET(connfd, &allset); /* добавление нового дескриптора в набор */

21  if (connfd > maxfd)

22   maxfd = connfd; /* для select() */

23  if (i > maxi)

24   maxi = i; /* максимальный индекс в массиве client[] */


25  return(--nready);

26 }

7-25
 Принимается соединение и используется первый свободный элемент массива
client
. Код данной функции скопирован из начала кода, приведенного в листинге 6.4. Если свободных элементов в массиве нет, мы закрываем новое соединение и занимаемся обслуживанием уже имеющихся клиентов.

Когда присоединенный сокет готов для чтения, вызывается функция

readablе_conn
(листинг 28.27), а ее аргументом является индекс данного клиента в массиве client.

Листинг 28.27. Считывание данных и, возможно, дескриптора от клиента

//icmpd/readable_conn.c

 1 #include "icmpd.h"


 2 int

 3 readable_conn(int I)

 4 {

 5  int unixfd, recvfd;

 6  char c;

 7  ssize_t n;

 8  socklen_t len;

 9  struct sockaddr_storage ss;


10  unixfd = client[i].connfd;

11  recvfd = -1;

12  if ((n = Read_fd(unixfd, &c, 1, &recvfd)) == 0) {

13   err_msg("client %d terminated, recvfd = %d", i, recvfd);

14   goto clientdone; /* вероятно, клиент завершил работу */

15  }


16  /* данные от клиента, должно быть, дескриптор */

17  if (recvfd < 0) {

18   err_msg("read_fd did not return descriptor");

19   goto clienterr;

20  }

Считывание данных клиента и, возможно, дескриптора

13-18
 Вызываем функцию
read_fd
, приведенную в листинге 15.9, для считывания данных и, возможно, дескриптора. Если возвращаемое значение равно нулю, клиент закрыл свою часть соединения, вероятно, завершив свое выполнение.

ПРИМЕЧАНИЕ

При написании кода пришлось выбирать, что использовать для связи между приложением и демоном — либо потоковый доменный сокет Unix, либо дейтаграммный доменный сокет Unix. Дескриптор сокета UDP может быть передан через любой доменный сокет Unix. Причина, по которой предпочтение было отдано потоковому сокету, заключается в том, что он позволяет определить момент завершения клиента. Все дескрипторы автоматически закрываются, когда клиент завершает работу, в том числе и доменный сокет Unix, используемый для связи с демоном, в результате чего данный клиент удаляется демоном из массива client. Если бы мы использовали сокет дейтаграмм, то не узнали бы, когда клиент завершил работу.

16-20
 Если клиент не закрыл соединение, ждем получения дескриптора. Вторая часть функции
readable_conn
приведена в листинге 28.28.

Листинг 28.28. Получение номера порта, который клиент связал с UDP-сокетом

//icmpd/readable_conn.c

21  len = sizeof(ss);

22  if (getsockname(recvfd, (SA*)&ss, &len) < 0) {

23   err_ret("getsockname error");

24   goto clienterr;

25  }


26  client[i].family = ss.ss_family;

27  if ((client[i].lport = sock_get_port((SA*)&ss, len)) == 0) {

28   client[i].lport = sock_bind_wild(recvfd, client[i].family);

29   if (client[i].lport <= 0) {

30    err_ret("error binding ephemeral port");

31    goto clienterr;

32   }

33  }

34  Write(unixfd, "1", 1); /* сообщение клиенту об успехе */

35  Close(recvfd); /* работа с UDP-сокетом клиента завершена */

36  return(--nready);


37 clienterr:

38  Write(unixfd, "0", 1); /* сообщение клиенту об ошибке */

39 clientdone:

40  Close(unixfd);

41  if (recvfd >= 0)

42   Close(recvfd);

43  FD_CLR(unixfd, &allset);

44  client[i].connfd = -1;

45  return(--nready);

46 }

Получение номера порта, связанного с сокетом UDP

21-25
 Вызывается функция
getsockname
, так что демон может получить номер порта, связанного с сокетом. Поскольку неизвестно, каков размер буфера, необходимого для размещения структуры адреса сокета, мы используем структуру
sockaddr_storage
, которая достаточно велика для структуры адреса сокета любого поддерживаемого системой типа и обеспечивает нужное выравнивание.

26-33
 Семейство адресов сокета вместе с номером порта сохраняется в структуре
client
. Если номер порта равен нулю, мы вызываем функцию
sock_bind_wild
для связывания универсального адреса и динамически назначаемого порта с сокетом, но, как отмечалось ранее, такой подход не работает в реализациях SVR4.

Сообщение клиенту об успехе

34
 Один байт, содержащий символ
"1"
, отправляется обратно клиенту.

Закрытие UDP-сокета клиента

35
 Заканчиваем работу с UDP-сокетом клиента и закрываем его с помощью функции
close
. Дескриптор был переслан нам клиентом и, таким образом, является копией; следовательно, UDP-сокет все еще открыт на стороне клиента.

Обработка ошибок и завершение работы клиента

37-45
 Если происходит ошибка, клиент получает нулевой байт. Когда клиент завершается, наша часть доменного сокета Unix закрывается, и соответствующий дескриптор удаляется из набора дескрипторов для функции
select
. Полю
connfd
структуры
client
присваивается значение -1, что является указанием на ее освобождение.

Функция

readable_v4
вызывается, когда символьный сокет ICMPv4 открыт для чтения. Первая часть данной функции приведена в листинге 28.29. Этот код аналогичен коду для ICMPv4, приведенному ранее в листингах 28.6 и 28.15.

Листинг 28.29. Обработка полученных дейтаграмм ICMPv4, первая часть

//icmpd/readable_v4.c

 1 #include "icmpd.h"

 2 #include 

 3 #include 

 4 #include 

 5 #include 


 6 int

 7 readable_v4(void)

 8 {

 9  int i, hlen1, hlen2, icmplen, sport;

10  char buf[MAXLINE];

11  char srcstr[INET_ADDRSTRLEN], dststr[INET_ADDRSTRLEN];

12  ssize_t n;

13  socklen_t len;

14  struct ip *ip, *hip;

15  struct icmp *icmp;

16  struct udphdr *udp;

17  struct sockaddr_in from, dest;

18  struct icmpd_err icmpd_err;


19  len = sizeof(from);

20  n = Recvfrom(fd4, buf, MAXLINE, 0, (SA*)&from, &len);


21  printf("%d bytes ICMPv4 from %s:", n, Sock_ntop_host((SA*)&from, len));


22  ip = (struct ip*)buf; /* начало IP-заголовка */

23  hlen1 = ip->ip_hl << 2; /* длина IP-заголовка */


24  icmp = (struct icmp*)(buf + hlen1); /* начало ICMP-заголовка */

25  if ((icmplen = n - hlen1) < 8)

26   err_quit("icmplen (%d) < 8", icmplen);


27  printf(" type = %d, code = %d\n", icmp->icmp_type, icmp->icmp_code);

Функция выводит некоторую информацию о каждом получаемом сообщении ICMP. Это было сделано для отладки при разработке демона, и вывод управляется аргументом командной строки.

В листинге 28.30 приведена вторая часть функции

readable_v4
.

Листинг 28.30. Обработка полученных дейтаграмм ICMPv4, вторая часть

//icmpd/readable_v4.c

28  if (icmp->icmp_type == ICMP_UNREACH ||

29   icmp->icmp_type ==ICMP_TIMXCEED ||

30   icmp->icmp_type == ICMP_SOURCEQUENCH) {

31   if (icmplen < 8 + 20 + 8)

32    err_quit("icmplen (%d) < 8 + 20 + 8, icmplen);


33   hip = (struct ip*)(buf + hlen1 + 8);

34   hlen2 = hip->ip_hl << 2;

35   printf("\tsrcip = %s, dstip = %s, proto = %d\n",

36    Inet_ntop(AF_INET, &hip->ip_src, srcstr, sizeof(srcstr)),

37    Inet_ntop(AF_INET, &hip->ip_dst, dststr, sizeof(dststr)),

38    hip->ip_p);

39   if (hip->ip_p == IPPROTO_UDP) {

40    udp = (struct udphdr*)(buf + hlen1 + 8 + hlen2);

41    sport = udp->uh_sport;


42    /* поиск доменного сокета клиента, отправка заголовка */

43    for (i = 0; i <= maxi; i++) {

44     if (client[i].connfd >= 0 &&

45      client[i].family == AF_INET &&

46      client[i].lport == sport) {


47      bzero(&dest, sizeof(dest));

48      dest.sin_family = AF_INET;

49 #ifdef HAVE_SOCKADDR_SA_LEN

50      dest.sin_len = sizeof(dest);

51 #endif

52      memcpy(&dest.sin_addr, &hip->ip_dst,

53       sizeof(struct in_addr));

54      dest.sin_port = udp->uh_dport;


55      icmpd_err.icmpd_type = icmp->icmp_type;

56      icmpd_err.icmpd_code = icmp->icmp_code;

57      icmpd_err.icmpd_len = sizeof(struct sockaddr_in);

58      memcpy(&icmpd_err.icmpd_dest, &dest, sizeof(dest));


59      /* преобразование кода и типа ICMP в значение errno */

60      icmpd_err.icmpd_errno = EHOSTUNREACH; /* по умолчанию */

61      if (icmp->icmp_type == ICMP_UNREACH) {

62       if (icmp->icmp_code == ICMP_UNREACH_PORT)

63        icmpd_err.icmpd_errno = ECONNREFUSED;

64       else if (icmp->icmp_code == ICMP_UNREACH_NEEDFRAG)

65        icmpd_err.icmpd_errno = EMSGSIZE;

66      }

67      Write(client[i].connfd, &icmpd_err, sizeof(icmpd_err));

68     }

69    }

70   }

71  }

72  return(--nready);

73 }

Проверка типа сообщения, уведомление приложения

29-31
 ICMP-сообщения, которые посылаются приложениям, — это сообщения о недоступности порта, превышении времени и завершении клиента (см. табл. 28.1).

Проверка ошибки UDP, поиск клиента

34-42
 Указатель
hip
указывает на IP-заголовок, который возвращается сразу после заголовка ICMP. Это IP-заголовок дейтаграммы, вызвавшей ICMP-ошибку. Мы убеждаемся, что эта IP-дейтаграмма является UDP-дейтаграммой, а затем извлекаем номер UDP-порта из UDP-заголовка, следующего за IP-заголовком.

43-55
 По всем структурам
client
осуществляется поиск подходящего семейства адресов и порта. Если соответствие найдено, строится структура адреса сокета IPv4, которая содержит IP-адрес получателя и порт из UDP-дейтаграммы, вызвавшей ошибку.

Построение структуры icmpd_err

56-70
 Строится структура
icmpd_err
, посылаемая клиенту через доменный сокет Unix. Тип и код сообщения ICMP сначала отображаются в значение
errno
, как показано в табл. 28.1.

Ошибки ICMPv6 обрабатываются функцией

readable_v6
, первая часть которой приведена в листинге 28.31. Обработка ошибок ICMPv6 аналогична коду, приведенному в листингах 28.7 и 28.16.

Листинг 28.31. Обработка полученной дейтаграммы ICMPv6, первая часть

//icmpd/readable_v6.c

 1 #include "icmpd.h"

 2 #include 

 3 #include 

 4 #include 

 5 #include 


 6 #ifdef IPV6

 7 #include 

 8 #include 

 9 #endif


10 int

11 readable_v6(void)

12 {

13 #ifdef IPV6

14  int i, hlen2, icmp6len, sport;

15  char buf[MAXLINE];

16  char srcstr[INET6_ADDRSTRLEN], dststr[INET6_ADDRSTRLEN];

17  ssize_t n;

18  socklen_t len;

19  struct ip6_hdr *ip6, *hip6;

20  struct icmp6_hdr *icmp6;

21  struct udphdr *udp;

22  struct sockaddr_in6 from, dest;

23  struct icmpd_err icmpd_err;


24  len = sizeof(from);

25  n = Recvfrom(fd6, buf, MAXLINE, 0, (SA*)&from, &len);


26  printf("%d bytes ICMPv6 from %s:", n, Sock_ntop_host((SA*)&from, len));


27  icmp6 = (struct icmp6_hdr*)buf; /* начало заголовка ICMPv6 */

28  if ((icmp6len = n) < 8)

29   err_quit("icmp6len (%d) < 8", icmp6len);


30  printf(" type = %d, code = %d\n", icmp6->icmp6_type, icmp6->icmp6_code);

Вторая часть функции

readable_v6
приведена в листинге 28.32. Код аналогичен приведенному в листинге 28.30: мы проверяем тип ICMP-ошибки, убеждаемся, что дейтаграмма, вызвавшая ошибку, является UDP-дейтаграммой, а затем строим структуру
icmpd_err
, которую отсылаем клиенту.

Листинг 28.32. Обработка полученной дейтаграммы ICMPv6, вторая часть

//icmpd/readable_v6.c

31  if (icmp6->icmp6_type == ICMP6_DST_UNREACH ||

32   icmp6->icmp6_type == ICMP6_PACKET_TOO_BIG ||

33   icmp6->icmp6_type == ICMP6_TIME_EXCEEDED) {

34   if (icmp6len < 8+8)

35    err_quit("icmp6len (%d) < 8 + 8", icmp6len);

36   hip6 = (struct ip6_hdr*)(buf + 8);

37   hlen2 = sizeof(struct ip6_hdr);

38   printf("\tsrcip = %s, dstip = %s, next hdr = %d\n",

39    Inet_ntop(AF_INET6, &hip6->ip6_src, srcstr, sizeof(srcstr)),

40    Inet_ntop(AF_INET6, &hip6->ip6_dst, dststr, sizeof(dststr)),

41    hip6->ip6_nxt);

42   if (hip6->ip6_nxt == IPPROTO_UDP) {

43    udp = (struct udphdr*)(buf + 8 + hlen2);

44    sport = udp->uh_sport;


45    /* поиск доменного сокета клиента, отправка заголовков */

46    for (i = 0; i <= maxi; i++) {

47     if (client[i].connfd >= 0 &&

48      client[i].family == AF_INET6 &&

49      client[i].lport == sport) {


50      bzero(&dest, sizeof(dest));

51      dest.sin6_family = AF_INET6;

52 #ifdef HAVE_SOCKADDR_SA_LEN

53      dest.sin6_len = sizeof(dest);

54 #endif

55      memcpy(&dest.sin6_addr, &hip6->ip6_dst,

56       sizeof(struct in6_addr));

57      dest.sin6_port = udp->uh_dport;


58      icmpd_err.icmpd_type = icmp6->icmp6_type;

59      icmpd_err.icmpd_code = icmp6->icmp6_code;

60      icmpd_err.icmpd_len = sizeof(struct sockaddr_in6);

61      memcpy(&icmpd_err.icmpd_dest, &dest, sizeof(dest));


62      /* преобразование типа и кода ICMPv6 к значению errno */

63      icmpd_err.icmpd_errno = EHOSTUNREACH; /* по умолчанию */

64      if (icmp6->icmp6_type == ICMP6_DST_UNREACH &&

65       icmp6->icmp6_code ICMP6_DST_UNREACH_NOPORT)

66       icmpd_err.icmpd_errno = ECONNREFUSED;

67      if (icmp6->icmp6_type == ICMP6_PACKET_TOO_BIG)

68       icmpd_err.icmpd_errno = EMSGSIZE;

69      Write(client[i].connfd, &icmpd_err, sizeof(icmpd_err));

70     }

71    }

72   }

73  }

74  return(--nready);

75 #endif

76 }

28.8. Резюме

Символьные сокеты обеспечивают три возможности:

1. Чтение и запись пакетов ICMPv4, IGMPv4 и ICMPv6.

2. Чтение и запись IP-дейтаграммы с полем протокола, которое не обрабатывается ядром.

3. Формирование своих собственных заголовков IPv4, обычно используемых в диагностических целях (или, к сожалению, хакерами).

Два традиционных диагностических средства — программы

ping
и
traceroute
— используют символьные сокеты. Мы разработали наши собственные версии этих программ, поддерживающие обе версии протокола — и IPv4, и IPv6. Также нами разработан наш собственный демон
icmpd
, который обеспечивает доступ к сообщениям об ошибках ICMP через сокет UDP. Данный пример также иллюстрирует передачу дескриптора через доменный сокет Unix между неродственными клиентом и сервером.

Упражнения

1. В этой главе говорилось, что почти все поля заголовка IPv6 и все дополнительные заголовки доступны приложению через параметры сокета или вспомогательные данные. Какая информация из дейтаграммы IPv6 не доступна приложению?

2. Что произойдет в листинге 28.30, если по какой-либо причине клиент перестанет производить считывание из своего доменного сокета Unix и демон

icmpd
накопит множество сообщений для данного клиента? В чем заключается простейшее решение этой проблемы?

3. Если задать нашей программе

ping
адрес широковещательной передачи, направленный в подсеть, она будет работать. То есть широковещательный эхо- запрос ICMP посылается как широковещательный запрос канального уровня, даже если мы не установим параметр сокета
SO_BROADCAST
. Почему?

4. Что произойдет с программой

ping
, если мы запустим ее на узле с несколькими интерфейсами, а в качестве аргумента имени узла возьмем групповой адрес 224.0.0.1?

Глава 29