Книга: Сетевое программирование. От основ до приложений
Назад: Глава 23. TUN/TAP-интерфейсы. Техника Kernel bypass
Дальше: Глава 25. Поиск ошибок и обнаружение места сбоя

Глава 24. Проблемы сетевых приложений, диагностика и отладка

Устаревший код часто отличается от предлагаемого на замену тем, что он действительно работает и масштабируется.

Бьерн Страуструп, «FAQ: What is legacy code», 2007

Введение

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

Мы сосредоточимся на проблемах в канале. И по причине того, что ключевым этапом в обнаружении ошибок является локализация их источника, узнаем, как искать точки возникновения проблем, а также как выяснить, где именно проблема — в канале или в приложении, — и научимся использовать инструменты для анализа сети. Для этого мы освоим утилиты трассировки, которые также помогут устранять проблемы на разных уровнях.

После этого сосредоточимся на окружении приложения и попытаемся на практике использовать различные инструменты.

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

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

Заблуждения относительно распределенных систем

Зачастую разработчики забывают или не знают про сложность, которую представляет сеть. Отсюда и появляются заблуждения, ведущие к ошибкам в приложениях. Наиболее распространенные из этих заблуждений были сформулированы Sun Microsystems:

• Сеть является надежной средой.

• Задержка передачи данных равна нулю.

• Пропускная способность бесконечна.

• Сеть защищена и безопасна.

• Топология сети не меняется.

• В сети есть только один администратор.

• Транспортные расходы равны нулю.

• Сеть однородна.

В одной из статей для Fog Creek Tech Talks приводилась интересная причина обрыва сети: акулы, которые перекусывают кабели. Кабель испускает электромагнитные волны, а также может вибрировать от взаимодействия полей. Акула замечает это и, считая кабель добычей, атакует его.

Это одна из причин того, что кабели, прокладываемые через океан, всегда имеют прочные, в том числе кевларовые, оболочки.

Еще один реальный случай — ошибка в прошивке целой партии маршрутизаторов. В результате маршрутизаторы портили данные, затем вычисляли правильные контрольные суммы и отправляли испорченные данные далее по маршруту.

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

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

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

• Медленный ответ на запросы пользователя. Зачастую, к тому же, ответ содержит ошибки, хотя если бы разработчики учли задержки передачи, этого можно было бы избежать.

• Потребление неограниченного трафика. Это увеличивает количество ошибок и отброшенных пакетов в приложениях, что свидетельствует о неэффективном расходе пропускной способности канала. Из-за этого возможно появление узких мест, когда один участок, через который проходят данные, замедляет всю сеть, не позволяя использовать канал с максимальной эффективностью.

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

• Разработка без учета изменений характеристик сети, в том числе изменения топологии. И соответственно, приложения под эти изменения не адаптируются. Об изменениях топологии топологии, вызванных отказами, мы поговорим в книге 2, в главе о CAP-теореме.

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

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

• Приложения не учитывают, что их данные чаще всего проходят через сети разных типов. Разные типы сетей означают разные характеристики: сравните магистральный канал T1 и домашнюю сеть Wi-Fi у пользователя. Кроме того, данные приходят через разные узлы: от смартфона под управлением Android до сервера под управлением Windows. Это приводит к тем же проблемам, что и первые три заблуждения.

Транспортные расходы TCP

Имеется «разогнавшееся» TCP-соединение, через которое на полной скорости осуществляется обмен данными. Вопрос: как часто, в пропорции к пакетам данных, принимающая сторона посылает ACK-пакеты?

Казалось бы, нечасто, с учетом больших окон, задержек ACK и т.п. На самом же деле в среднем подтверждается каждый второй пакет.

Причина в том, что основное событие, которое заставляет передатчик послать следующий пакет, — это не срабатывание таймера, а получение подтверждения, которое открывает окно для следующих пакетов. Поэтому подтверждений TCP отправляет много, являясь, по сути, протоколом ACK-paced (управляемым ACK).

Одно из практических следствий этого факта: если Wi-Fi обеспечивает заявленную скорость в 54 Мбит, то фактическая скорость, достижимая для TCP между станцией и точкой доступа, составляет приблизительно 22 Мбит, а между станциями — еще меньше.

Wi-Fi использует довольно большие зазоры между пакетами плюс плотный поток TCP ACK, который с учетом этих зазоров будет занимать существенную часть полосы. В результате пропускная способность для передачи полезных данных составит чуть меньше половины от исходной.

Точки возникновения проблем

Сеть состоит из узлов и связей между ними, поэтому проблемы могут возникать в следующих местах:

непосредственно в узлах, образующих сеть;

связях и каналах, их обеспечивающих.

Узлы сети либо участвуют в обмене непосредственно, например, как сервер, поставляющий ресурсы, и клиент, их потребляющий, или служат транзитными узлами, как маршрутизаторы. В норме будем считать, что транзитные узлы являются частью канала связи. То есть канал связи является частью канала передачи данных и образуется:

транзитными узлами, большинство из которых составляют маршрутизаторы;

средой передачи.

Возможные неполадки в каналах:

Обрывы, шумы в среде передачи: проводах, оптических волокнах, помехи в эфире и т.п.

• Некорректная конфигурация маршрутизаторов.

Перегрузка канала связи.

Возможные неполадки в узлах:

Некорректная конфигурация сети.

Ошибки в реализации прикладного сетевого ПО.

Проблемы с аппаратурой, например физические неисправности сетевого интерфейса.

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

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

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

Неполадки в транзитных узлах и в среде передачи могут привести к разделению сети на изолированные сегменты, то есть к разрушению сети.

Это книга не по сетевому администрированию, и поэтому следующие темы мы не рассматриваем:

• неполадки в каналах;

• ошибки в конфигурации узлов.

Их настройку и работу обеспечивает сетевой администратор или обладающий теми же навыками специалист. В современных системах, где конфигурация сети выполняется автоматически, с использованием протоколов типа DHCP, технологий ZeroConf, UPnP и подобных, тоже может требоваться настройка, которую также выполняет сетевой администратор. Потому что даже в случае реализации серверов и других компонентов программистом ответственность за работу сети несет администратор.

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

• Как понять, что ошибка — в реализации приложения.

• Как локализовать и устранить данную ошибку.

Поиск точки возникновения проблем

Обычно первая задача — понять, что ошибка кроется в приложении. Затем следует либо отладка приложения, либо обращение к сетевому администратору. Выяснить наличие ошибки можно путем исключения.

Обычно легче проверить и затем исключить сеть до начала поиска ошибок в приложении. Чтобы выяснить, есть ли проблемы в канале, можно использовать некоторые простейшие меры для его исключения:

Попробовать другие каналы. Например, если узел подключается в сеть по корпоративному Ethernet, попробуйте Wi-Fi или другую сеть. Это не всегда возможно, но если проблема повторяется, она, вероятно, не в канале.

• Попробовать выполнить диагностику канала, используя специализированные инструменты, рассмотренные далее, например тот же netstress.

• Запустить сервер локально и проверить связь через интерфейс локальной петли. Если проблема там, это точно не проблема канала.

Эмулировать стенд на виртуальных машинах в рамках одной физической машины и проверить работоспособность на этом стенде.

Но обычно достаточно посмотреть, что передается и что приходит. Для этого можно использовать генератор сообщений: простой клиент, отправляющий серверу последовательности ASCII-символов.

Существует множество таких решений, например , , и подобные. Встроенный генератор пакетов также имеет рассмотренный в предыдущей главе фреймворк DPDK.

Некоторые системные утилиты

В составе операционных систем, как правило, уже имеются некоторые средства диагностики, которые мы рассмотрим далее.

Среди прочих отдельно выделим:

Обычный ping — самый доступный инструмент тестирования.

• Клиент-серверные утилиты тестирования:

— утилиты, позволяющие тестировать канал по TCP и UDP, создавая как однонаправленный поток от клиента до сервера, так и двунаправленный.

— его часто используют для тестирования пропускной способности маршрутизаторов.

Ping

Плюс обычного ping в том, что на большинстве систем он уже присутствует и его не требуется устанавливать.

Одновременно плюсом и минусом является работа по ICMP:

• Большинство межсетевых экранов пропускают сообщения ICMP Ping.

• Он позволяет работать, не запуская отдельный сервер.

Но проверять он позволяет только сетевой уровень, и нагрузочное тестирование сети, используя ping, выполнить затруднительно, хотя и реально. А проверку особенностей настройки TCP — вообще невозможно.

Типичная утилита ping :

Задать шаблон отправляемых данных через опцию -p. Например, -p ff для отправки только единиц. Шаблон в данном случае описывает байт шестнадцатеричным числом. То есть появляется возможность увидеть искажения шаблона на принимающей стороне. Опцию поддерживают не все реализации утилиты.

• Задать размер пакета через опцию -s, чтобы определить, какой предельный размер можно передать через канал.

• Управлять фрагментацией IP-дейтаграмм, используя опцию -M.

• Задавать исходящим пакетам тег через опцию -m, чтобы, например, использовать разные политики маршрутизации.

• Включать запись отладочной информации нижележащих уровней, используя опцию -d. Это полезно для отладки сетевого стека. Опция ставит на сокет флаг SO_DEBUG. Правда, в ядре Linux данный флаг в большинстве случаев не будет иметь никакого эффекта.

• Выполнять «адаптивный пинг» — адаптирует RTT так, чтобы в сети был только один запрос, что для быстрых сетей обеспечивает лавинную рассылку и позволяет выполнить нагрузочное тестирование ICMP-пакетами. Включается опцией -A.

Выполнять лавинную рассылку также с целью нагрузочного тестирования. В Linux ping — опция -f.

Подробнее об этом см. в man 8 ping.

Кроме того, отдельные реализации поддерживают другие полезные возможности. Поэтому диагностику стоит начать с запуска ping.

Пример запуска ping с шаблоном данных:

ping localhost -p a1f1

PATTERN: 0xa1f1

PING localhost(localhost.localdomain (::1)) 56 data bytes

64 bytes from localhost.localdomain (::1): icmp_seq=1 ttl=64 time=0.051 ms

64 bytes from localhost.localdomain (::1): icmp_seq=2 ttl=64 time=0.065 ms

64 bytes from localhost.localdomain (::1): icmp_seq=3 ttl=64 time=0.025 ms

64 bytes from localhost.localdomain (::1): icmp_seq=4 ttl=64 time=0.079 ms

^C

--- localhost ping statistics ---

4 packets transmitted, 4 received, 0% packet loss, time 3032ms

rtt min/avg/max/mdev = 0.025/0.055/0.079/0.019 ms

В последней строке указано время с момента отправки запроса до получения ответа. В конце показано количество отправленных пакетов и пришедших ответов: если числа равны, значит, потерь нет.

В окне Wireshark на рис. 24.1 видно, что была отправлена дейтаграмма, причем данные без искажений.

Рис. 24.1. Дамп сетевых пакетов, отправленных ping

Внимание! В данном случае по умолчанию используется IPv6, что видно по адресу ::1. Поэтому и протокол, который использует Ping, — ICMPv6, а не ICMP. При необходимости можно явно указать IPv4-адрес узла, в таком случае будут видны пакеты ICMPv4.

Размер данных — 40 байт, но в реальности приходит 56 байт. Еще 16 байт — это структура timeval, которая содержит временную метку, возвращаемую функцией gettimeofday().

Также можем использовать ping, реализованный ранее:

sudo build/bin/b01-ch04-ping-from-root localhost

Pinging "localhost.localdomain" [127.0.0.1]

Raw socket was created...

Starting to send packets...

TTL = 255

Recv timeout seconds = 1

Recv timeout microseconds = 0

Sending packet 0 to "localhost" request with id = 35448

Sending packet 1 to "localhost" request with id = 35448

Receiving packet 0 from "localhost" response with id = 35448, time = 0.01ms

Sending packet 2 to "localhost" request with id = 35448

Внимание! Напомним, что хотя ping на Linux может работать через установку привилегии либо, на новых версиях, используя специальный вариант создания ICMP-сокета, обычно утилита использует raw-сокеты и требует прав root.

В дампе на рис. 24.2 видно, что приходит «зашитая» последовательность символов — повторение шаблона 0xa1f1. Та же последовательность, как правило, отправляется сетевым стеком в ответе:

Рис. 24.2. Ответ на ping

В «семействе» ping существует несколько утилит, которые выполняют сходные действия:

— для работы по протоколу ARP в рамках сегмента ЛВС.

— для массового пингования хостов по списку.

— проверяет, доступен ли нужный порт TCP.

Сходные решения существуют и для ОС Windows как отдельные утилиты. Например, утилита от Sysinternals, которая может работать и с UDP, или от Google, а также наборы скриптов PowerShell.

Netstat и SS

Netstat выводит информацию о сетевой подсистеме. В Linux программа Netstat является устаревшей, и в качестве замены предлагается использовать утилиту SS, речь о которой пойдет ниже. Однако Netstat до сих пор можно встретить в множестве разных систем.

Тип выводимых данных задается ключами режимов:

• Режим по умолчанию — список открытых сокетов.

• Режим --route или -r — таблицы маршрутизации ядра.

• Режим --groups или -g — состав групп многоадресной рассылки.

• Режим --interfaces или -i — список интерфейсов.

• Режим --statistics или -s — статистика по каждому протоколу. Например, позволяет узнать, сколько было получено IP-пакетов с ошибками в IP-заголовке.

Часто Netstat удобно вызывать с параметрами -nlp4, где:

-n — выводить IP-адреса вместо имен узлов. Это быстрее благодаря отсутствию обращения к DNS. К тому же часто удобнее видеть адреса.

• -l — выводить только сокеты, которые открыты на прослушивание. Обычно требуется не список всех исходящих соединений из системы, а только список открытых портов. Эта опция их выводит.

• -p — выводить PID и пути к процессам, открывшим сокет.

-4 — выводить только сокеты протокола IPv4. Для IPv6 следует задать параметр -6, для Unix-сокетов — -x, а для остальных см. man 8 netstat.

Помимо этих опций часто могут использоваться следующие:

-t — отображать TCP-сокеты.

-u — отображать UDP-сокеты.

Пример вывода Netstat для IPv6:

netstat -nlp6

(Not all processes could be identified, non-owned process info

will not be shown, you would have to be root to see it all.)

Active Internet connections (only servers)

Proto Recv-Q Send-Q Local Address           Foreign Address         State PID/Program name   

tcp6       0      0 :::5355                 :::*                    LISTEN

tcp6       0      0 :::1716                 :::*                    LISTEN 9259/kdeconnectd

udp6       0      0 :::1716                 :::*

9259/kdeconnectd   

udp6       0      0 :::5353                 :::*

udp6       0      0 :::5355                 :::*                  

udp6       0      0 :::40207                :::*

udp6       0      0 :::48827                :::*

3016335/app.asar --

raw6       0      0 :::58                   :::*

Утилита SS — Socket Statistics — является более современной заменой Netstat. В Linux она показывает информацию, похожую на ту, которую выводит Netstat, только в несколько другом виде и более подробно.

Ключи данных приложений во многом совпадают, но ss не показывает таблицы маршрутизации и прочее, она заменяет только режим Netstat по умолчанию. Эта утилита работает напрямую через подсистему Netlink, тогда как более старый Netstat работает через ProcFS.

Пример вывода ss для IPv6:

ss -nlp6

Netid  State  Recv-Q  Send-Q  Local Address:Port    Peer Address:Port  Process

icmp6 UNCONN  0       0       *:58                  *:*

udp   UNCONN  0       0       [::]:5353             [::]:*

udp   UNCONN  0       0       [::]:5353             [::]:*

udp   UNCONN  0       0       [::]:5355             [::]:*

udp   UNCONN  0       0       [::]:41798            [::]:*

tcp   LISTEN  0       4096    [::1]:631             [::]:*

tcp   LISTEN  0       4096    [::]:5355             [::]:*

tcp   LISTEN  0       50      [::ffff:127.0.0.1]:29 *:* "          kde-plasma

В ОС Windows похожей функциональностью обладает утилита от SysInternals. Она также позволяет видеть создание и уничтожение новых сокетов «в режиме онлайн».

Trace

Также можно изучить канал с помощью . Вывод утилиты нагляден:

traceroute -w 15 ya.ru

traceroute to ya.ru (87.250.250.242), 30 hops max, 60 byte packets

1  router (192.168.2.1)  0.685 ms  1.222 ms  1.581 ms

2  0-0-b2.ysl.severttk.ru (80.92.20.1)  2.728 ms  3.276 ms  3.642 ms

3  sever-ttk.transtelecom.net (217.150.42.18)  4.159 ms  4.494 ms  5.546 ms

4  * * *

5  yandexspb-gw.transtelecom.net (217.150.63.225)  15.390 ms  17.645 ms  18.660 ms

6  sas-32z3-ae1.yndx.net (87.250.239.183)  31.749 ms vla-32z1-ae3.yndx.net (93.158.160.151)  20.712 ms *

7  ya.ru (87.250.250.242)  15.357 ms *  15.486 ms

Утилита показывает маршрут пакетов и количество миллисекунд до каждого промежуточного узла, что позволяет оценить скорость прохождения различных участков маршрута и увидеть, на каком из узлов возникают задержки передачи. Параметр -w 15 говорит о том, что ответ на зондирующий пакет надо ждать 15 секунд.

В Windows подобной функциональностью обладают утилиты и . Одна из них обычно поставляется с ОС, какая — зависит от версии Windows.

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

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

Как работает Traceroute

Traceroute отправляет последовательность зондирующих пакетов целевому узлу. Обычно в реализациях для Unix-подобных ОС это UDP-пакеты с портами из диапазона от 33 434 до 33 534, как показано на рис. 24.3, но могут быть запросы ICMP Echo, дейтаграммы ICMPv6 Echo, UDP Lite или пакеты TCP SYN. В утилите tracert из состава ОС Windows доступны только ICMP или ICMPv6.

Рис. 24.3. Работа traceroute

У первого зондирующего пакета TTL установлен в 1, так что первый же транзитный узел вернет ICMP-ответ с кодом 11 «Time-to-live exceeded in transit».

Второй пакет Traceroute отправляет с TTL, равным 2. Согласно протоколу, каждый узел должен уменьшать TTL на 1. Поэтому такой пакет отбрасывается уже вторым транзитным узлом. И так далее.

Время между запросом и ответом фиксируется и выводится.

Когда TTL пакета становится достаточным, он достигает целевого узла. В этом случае ICMP-ответ меняется. В случае UDP или TCP зондирующих пакетов — на 3 «Destination unreachable (Port unreachable)» или на 0 «Echo reply», если использовался запрос ICMP Echo.

Изменившийся ответ является условием для остановки работы Traceroute.

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

Работа с доменными именами

Утилита Dig — Domain Information Groper, которую мы уже использовали в главе 2, позволяет выполнить запрос к DNS-серверу:

dig mail.com

 

; <<>> DiG 9.18.5 <<>> mail.com

;; global options: +cmd

;; Got answer:

;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 58796

;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

 

;; OPT PSEUDOSECTION:

; EDNS: version: 0, flags:; udp: 65494

;; QUESTION SECTION:

;mail.com.                      IN      A

 

;; ANSWER SECTION:

mail.com.               300     IN      A       82.165.229.87

 

;; Query time: 50 msec

;; SERVER: 127.0.0.53#53(127.0.0.53) (UDP)

;; WHEN: Sun Oct 02 15:47:35 MSK 2022

;; MSG SIZE  rcvd: 53

Dig является частью пакета DNS-сервера , который доступен и для ОС Windows. Поэтому dig можно использовать, предварительно установив пакет либо отдельно скачав только утилиту.

Утилита Whois делает запрос к базе регистратора домена и позволяет узнать массу информации:

whois mail.com

  Domain Name: MAIL.COM

  Registry Domain ID: 3083119_DOMAIN_COM-VRSN

  Registrar WHOIS Server: whois.world4you.com

  Registrar URL: http://www.world4you.com

  Updated Date: 2023-05-30T14:58:40Z

  Creation Date: 1997-03-24T05:00:00Z

  Registry Expiry Date: 2025-03-25T04:00:00Z

  Registrar: World4You Internet Services GmbH

  Registrar IANA ID: 1476

  Registrar Abuse Contact Email:

  Registrar Abuse Contact Phone:

  Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited

  Name Server: NS-GMX.UI-DNS.BIZ

  Name Server: NS-GMX.UI-DNS.COM

  Name Server: NS-GMX.UI-DNS.DE

  Name Server: NS-GMX.UI-DNS.ORG

  DNSSEC: signedDelegation

  DNSSEC DS Data: 55057 8 2 AD94C93CADAB166458C96CECF151A9C937C835D1377FBB2219793458225BF8F0

  URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/

>>> Last update of whois database: 2024-07-16T22:35:19Z <<<

В ОС Windows утилита Whois и может быть скачана отдельно.

Существует также несколько простых утилит, например:

dnsdomainname — выводит доменную часть FQDN-узла, на котором утилита была запущена.

• hostname — выводит имя узла.

nslookup — утилита, похожая на dig. Используется для получения записей домена с DNS-серверов. Может работать как в интерактивном, так и в пакетном режиме. Она более старая, чем dig, но присутствует в большей части Unix-подобных ОС.

Вместе с ними в пакете hostname также могут содержаться утилиты nisdomainname, ypdomainname и т.п. Эти утилиты уже устарели.

Утилиты администратора

Эти утилиты пригодятся скорее администратору либо разработчику при настройке сети:

arp — управляет ARP кэшем IPv4. По умолчанию выведет содержимое кэша.

• ifconfig — утилита для управления сетевыми интерфейсами. Позволяет включать и выключать сетевые интерфейсы, задавать им адреса, устанавливать параметры и т.д. В Linux является устаревшей, замена ей — утилита ip.

• route — утилита для управления IP-маршрутами.

• ifup, ifdown, ifquery — включить, выключить интерфейс или запросить его состояние соответственно. Устарели.

ip — утилита, которая заменяет многие предыдущие устаревшие. О ней можно прочесть в man 8 ip. Вот несколько ее наиболее часто используемых режимов:

ip addr — вывод списка интерфейсов, а также управление IP-адресами;

ip link и ip stats — управление канальным уровнем и сетевыми интерфейсами, просмотр статистики интерфейсов;

ip neighbour и ip ntable — управление ARP-кэшем;

ip route — управление маршрутами;

ip tuntap — управление TUN/TAP-устройствами.

dhclient — клиент DHCP. Чаще всего запускается автоматически. Но иногда полезен для того, чтобы получить адреса и другие параметры.

• ethtool — утилита для управления драйверами сетевых интерфейсов.

mii-tool — управляет Media Independent Interface.

В Windows для управления сетевыми интерфейсами применяются следующие утилиты:

ipconfig — управляет сетевыми интерфейсами:

• /renew — обновить адрес;

• /release — освободить адрес, то есть отправить сообщение DHCPRELEASE серверу DHCP;

• /setclassid, /showclassid — изменить либо показать код класса DHCP для адаптера;

• /flushdns, /registerdns, /displaydns — управление DNS-кэшем и его отображение.

• net — предоставляет доступ к различным сетевым сервисам, от отправки сообщений до управления сетевыми принтерами.

netsh — конфигурирует сетевые параметры. Именно это основная команда, аналог Linux-утилит ifconfig и ip. Может работать в режиме интерактивной оболочки и выполнять настройку как локального, так и удаленного узла.

При вызове без параметров ipconfig выводит данные о подключениях:

PS C:\> ipconfig

 

Windows IP Configuration

 

Ethernet adapter Ethernet:

 

   Connection-specific DNS Suffix  . :

   IPv4 Address. . . . . . . . . . . : 192.168.3.254

   Subnet Mask . . . . . . . . . . . : 255.255.255.0

   Default Gateway . . . . . . . . . : 192.168.3.3

Команда netsh, вызванная без параметров, запустит интерактивную оболочку; в противном случае выполнит заданную команду.

Так, например, можно отобразить список всех сокетных провайдеров:

PS C:\> netsh winsock show catalog

 

Winsock Catalog Provider Entry

------------------------------------------------------

Entry Type:                         Base Service Provider

Description:                        MSAFD Tcpip [TCP/IP]

Provider ID:                        {E70F1AA0-AB8B-11CF-8CA3-00805F48A192}

Provider Path:                      %SystemRoot%\system32\mswsock.dll

Catalog Entry ID:                   1001

Version:                            2

Address Family:                     2

Max Address Length:                 16

Min Address Length:                 16

Socket Type:                        1

Protocol:                           6

Service Flags:                      0x20066

Protocol Chain Length:              1

 

Winsock Catalog Provider Entry

------------------------------------------------------

Entry Type:                         Base Service Provider

Description:                        MSAFD Tcpip [UDP/IP]

Provider ID:                        {E70F1AA0-AB8B-11CF-8CA3-00805F48A192}

Provider Path:                      %SystemRoot%\system32\mswsock.dll

Catalog Entry ID:                   1002

Version:                            2

Address Family:                     2

 

...

Или добавить второй DNS-сервер к подключению:

PS C:\> netsh interface ip add dns "Подключение по локальной сети" 8.8.8.8

Все команды покажут справку, если передать им аргумент /?. А netsh в интерактивном режиме покажет справку по команде help.

Команды Netsh разделяются на группы:

winsock — группа команд управления Window Sockets;

interface — управление сетевыми интерфейсами;

firewall — управление межсетевым экраном;

и т.п.

Полезной командой в некоторых группах является команда reset, сбрасывающая настройки; например, при выполнении netsh winsock reset catalog будет сброшен каталог WinSock, то есть набор провайдеров.

Также для настройки сети можно использовать Microsoft System Console, или MSC, оснастки. Например, оснастка dhcpmgmt.msc управляет DHCP-сервером, а оснастки domain.msc, dssite.msc, dsa.msc управляют настройками домена и Active Directory.

Вызвать оснастки можно, просто набрав их имя с расширением в консоли.

Межсетевой экран

Иногда требуется отключить межсетевой экран, чтобы проверить, не блокирует ли он подключения. Сейчас в популярных ОС межсетевой экран уже встроен, и в каждой ОС он свой. Средства управления им зачастую тоже свои. Например, в Linux за управление межсетевым экраном Netfilter отвечает утилита iptables, а во FreeBSD встроен межсетевой экран IPFW, он конфигурируется утилитой ipfw. Межсетевой экран Packet Filter в OpenBSD, который используется также в MacOS X и стал основой для межсетевого экрана Core Force в ОС Windows, управляется через pfctl.

Чтобы скрыть все эти особенности, а также для упрощения работы с правилами часто используют более высокоуровневые утилиты, работающие в разных операционных системах и поддерживающие несколько разных межсетевых экранов. Например, существует инфраструктура NFTables, простые консольные утилиты, такие как UFW, Shorewall, Arno's Iptables Firewall, и множество графических интерфейсов, например firewall-config и Firewalld — демон и GUI, которые позволяют настраивать различные зоны для межсетевого экрана в RedHat, Firewall Builder, Gufw и т.д.

В Windows управление межсетевым экраном осуществляется через графический интерфейс, а также команду firewall утилиты netsh.

Перед тем как настраивать или отключать межсетевой экран через низкоуровневые команды, лучше выяснить, что используется для управления им в данной системе, и изучить документацию.

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

Снифферы

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

Также для отладки полезны отладочные прокси-серверы, которые были рассмотрены в главе 21.

Прочие инструменты

Если простая диагностика через ping не дала результата, можно обратиться к более сложным инструментам:

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

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

— фреймворк на Python, который используется для сборки произвольных сетевых пакетов.

Большинство этих утилит требуют от пользователя определенного понимания сетей и протоколов.

Если даже названные выше средства не помогли локализовать проблему, стоит обратиться к сетевому администратору. Он может использовать аппаратные средства для измерения параметров канала на физическом и канальном уровнях. К тому же у сетевых администраторов, как правило, есть доступ к промежуточному оборудованию, что позволяет выполнить более полную диагностику сети.

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

Трассировка

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

Утилиты strace, ltrace и им подобные

Утилита используется для того, чтобы увидеть, с какими параметрами выполняются реальные системные вызовы. Она построена вокруг либо аналогичного. Работает эта утилита и в BSD-системах. Трассировка вызовов — в целом широко используемая техника, независимо от конкретной ОС.

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

Во FreeBSD существует утилита truss, аналогичная strace.

В ОС Windows утилиты strace и ltrace может заменить из набора Debugging Tools for Windows, который входит в состав WDK или SDK.

Существует несколько других подобных утилит:

• Кросс-платформенная .

— графическая утилита под ОС Windows, которая показывает стек вызовов процесса.

для ОС Windows. Показывает и интерпретирует вызовы, отображая результаты в структурированном и понятном виде.

для ОС Windows.

• DTrace. Трассировщик уровня ядра. и для . Во FreeBSD, например, модуль идет в составе ядра. На Linux и Windows работает не всегда и требует установки драйвера.

для Linux. Более мощная альтернатива DTrace. В ОС Windows есть похожий механизм — , который мы рассмотрим подробнее в разделе «Трассировка WinSock».

— трассировщик для WinSock.

Работу с strace мы рассмотрим далее в этой главе.

Проект BCC

В Linux многие трассировщики построены на основе eBPF, как, например, tcptracer-bpf, позволяющий отслеживать события открытия и закрытия сокета, установки соединения, изменения режима и прочие. В отличие от strace, эти утилиты работают полностью на уровне ядра, а не через один системный вызов.

Рис. 24.4. Утилиты BPF для сети

Одним из заметных проектов, объединяющих утилиты подобного рода, является , часть которого показана на рис. 24.4. Это набор основанных на eBPF инструментов для создания эффективных программ трассировки ядра и управления им.

Проект, помимо сетевых функций, предлагает следующее:

Получение статистики:

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

• файловых систем: отслеживание медленных запросов и операций, причины удаления файлов. Позволяет выполнять трассировку вызова open(). Содержит утилиту, предоставляющую для файлов функциональность, аналогичную утилите top;

• блокировок мьютексов и семафоров, измерение времени прерываний;

• кэша памяти ядра SLAB/SLUB: коэффициента попаданий и промахов кэша страниц, в том числе по каждому процессу. Показывает незавершенные выделения памяти. Может искать утечки памяти;

• виртуальных машин KVM;

• использования процессора;

• процессов. Дает возможность обнаруживать длительные задержки планирования процессов, считать кэш-промахи по процессам. Может внедрять ошибки для того, чтобы выяснить поведение системы, обнаруживать потенциальные взаимоблокировки.

• Отслеживание медленных запросов сервера MySQL и PostgreSQL с отображением задержек.

• Получение данных о программах, реализованных на языках высокого уровня:

• блок-схемы методов;

• отслеживание событий сборки мусора в разных языках;

• отслеживание событий создания потоков в Java.

• Общее профилирование:

• медленных вызовов ядра или пользовательских функций;

• возможностей безопасности;

• произвольных функций с помощью фильтров;

• вывода с устройства tty или pts.

Построение гистограмм данных.

Набор инструментов очень полезен для выявления проблем в работающей системе. Например, если в приложении используется СУБД, время запроса может возрастать из-за медленного обращения к ней. BCC позволит легко измерить это время.

Приведем список утилит для исследования сетевого взаимодействия, находящихся в tools:

gethostlatency — показать задержку для вызовов getaddrinfo() и gethostbyname().

• bindsnoop — трассировка системных вызовов bind() для IPv4 и IPv6.

• tcptracer — отслеживать установку и завершение TCP-соединений: connect(), accept(), close().

tcpcong — отслеживать продолжительность состояния управления перегрузкой сокета TCP.

tcptop — показать суммарную пропускную способность отправки/получения TCP по узлам. Утилита top для TCP.

tcpsynbl — показать бэклог TCP SYN.

tcpaccept — отслеживать пассивные соединения TCP, то есть вызовы accept().

tcpconnect — отслеживать активные TCP-соединения, то есть connect().

tcpconnlat — отслеживать задержку активного соединения TCP.

tcpdrop — трассировать отброшенные ядром TCP-сегменты.

tcplife — отслеживать сеансы TCP и получать суммарную статистику.

tcpretrans — отслеживать повторные передачи TCP и TLP — Transaction Layer Packets, которые используются в PCI Express и служат, в том числе, для общения с адаптером.

tcprtt — отслеживать время приема-передачи TCP.

tcpstates — отслеживать изменения и продолжительность состояний TCP-сеанса.

tcpsubnet — суммировать отправленные пакеты TCP по подсетям.

sslsniff — прослушивать записанные и прочитанные данные OpenSSL.

• sofdsnoop — отслеживать файловые дескрипторы, прошедшие через Unix-сокеты.

netqtop — отслеживать и отображать распределения пакетов в очередях сетевых карт.

Большинство инструментов реализуются на Python и на C, для них доступны хорошие примеры и документация, поэтому можно легко реализовать инструменты под свои задачи.

Утилита tcptrace

Утилита для анализа дампов PCAP. Выводит различную информацию о соединении:

• продолжительность соединения;

• отправленные и полученные байты и сегменты;

• количество повторных передач;

• RTT;

• изменения TCP-окон;

• данные о пропускной способности.

Она также может создавать графики для дальнейшего анализа. Начиная с версии 5, в дополнение к возможностям TCP реализована минимальная обработка UDP.

Используя tcptrace, удобно собирать данные с нескольких TCP-потоков, по которым приложение ведет обмен.

Утилита iptables-tracer

Утилита позволяет отслеживать распределение пакетов по цепочкам iptables. Она добавляет точки трассировки в конфигурацию.

С ее помощью можно отфильтровать необходимый трафик, используя фильтры, как в PCAP:

iptables-tracer -f "-s 192.168.2.1 -p tcp --dport 443" -t 60s

14:42:00.284882 raw    PREROUTING   0x00000000 IP 192.168.2.1.36028 > 203.0.113.41.443: Flags [S], seq 3964591400, win 29200, length 0  [In:eth0 Out:]

14:42:00.287255 mangle PREROUTING   0x00008000 IP 192.168.2.1.36028 > 203.0.113.41.443: Flags [S], seq 3964591400, win 29200, length 0  [In:eth0 Out:]

14:42:00.288966 nat    PREROUTING   0x00008000 IP 192.168.2.1.36028 > 203.0.113.41.443: Flags [S], seq 3964591400, win 29200, length 0  [In:eth0 Out:]

Утилита nsntrace

Выполняет трассировку сетевой активности указанного приложения.

Создает виртуальные интерфейсы, запускает указанное приложение и записывает отфильтрованный через libpcap-трафик, например, в файл. Затем дамп можно обрабатывать любой стандартной утилитой, поддерживающей работу с PCAP.

Трассировка WinSock

Трассировка WinSock — это механизм для устранения неполадок, позволяющий отслеживать события на сокетах в ОС Windows. Он поддерживается, начиная с ОС Windows Vista.

События записываются в журнал. Существует два уровня трассировки:

информационный;

подробный.

На информационном уровне записываются следующие события:

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

• Привязка адреса — регистрируется связанный IP-адрес.

• Соединение — регистрируется IP-адрес удаленного абонента.

• Прерывания и отмены, вызванные WinSock, — событие регистрируется, когда WinSock прерывает или отменяет запрос.

• Сброс, инициированный провайдером транспорта, — событие регистрируется, когда провайдер транспорта указывает, что соединение было сброшено.

• Ошибки отправки и получения данных.

Разрыв связи и закрытие дескриптора сокета.

На подробном уровне кроме них записываются:

Отправка и получение буферов — регистрируются адрес и длина пользовательского буфера.

• Изменение опций сокета — событие возникает, когда приложение изменяет некоторые опции сокета, например SO_SNDBUF, SO_RCVBUF, SIO_ENABLE_CIRCULAR_QUEUEING, FIONBIO.

• Вызовы асинхронного ожиданияWSAPoll() и select().

• Маски событий — регистрируется маска события, переданная функции WSAEventSelect().

Дейтаграммы — каждое поступление дейтаграммы, если в буфере нет места для ее копирования.

Запустить трассировку можно следующей командой:

C:\> logman start -ets mywinsocksession -o winsocklogfile.etl -p Microsoft-Windows-Winsock-AFD

The command completed successfully.

В файл winsocklogfile.etl будут записаны события провайдера, указанного через опцию -p. Параметр mywinsocksession — название сессии, которое в дальнейшем используется для управления трассировкой.

Узнать список провайдеров можно, используя команду logman query providers.

Провайдеров достаточно много. Для сети могут быть полезны Microsoft-Windows-TCPIP, провайдеры из семейства Microsoft-Windows-Network*, например Microsoft-Windows-NetworkStatus, и некоторые другие.

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

C:\> logman stop -ets mywinsocksession

The command completed successfully.

Программа tracerpt.exe конвертирует бинарный протокол в текстовый:

C:\> tracerpt.exe winsocklogfile.etl -o winsockcatalogtracelog.xml

 

Input

----------------

File(s):

      winsocklogfile.etl

 

100.00%

 

Output

----------------

DumpFile:        winsockcatalogtracelog.xml

 

Warning:

Some events do not match the schema.

Please rerun the command with -lr to get less restricted XML dump

The command completed successfully.

Будет сгенерирован XML-файл, который содержит все события:

<RenderingInfo Culture="ru-RU">

<Level>Information </Level>

<Opcode>Connected </Opcode>

<Keywords>

<Keyword>Datagram socket </Keyword>

<Keyword>Winsock initiated event </Keyword>

</Keywords>

<Task>AfdSendToWithAddress</Task>

<Message>

sendto: 0: Process 0xFFFFA309A99F2080, Endpoint 0xFFFFA309A91FE890, Buffer Count 1, Buffer 0xFFFFA309AD17CF68, Length 30, Addr 224.2.2.2:8995, Seq 3100, Status STATUS_SUCCESS

</Message>

<Channel>

Microsoft-Windows-Winsock Network Event/Operational

</Channel>

<Provider>Microsoft-Windows-Winsock Network Event </Provider>

</RenderingInfo>

Также для просмотра можно использовать Event Viewer, который поставляется вместе с ОС.

Подробнее о трассировке и параметрах событий см. в разделе MSDN.

Wsock-trace

Помимо встроенного механизма трассировки существует бесплатный трассировщик с открытым исходным кодом — Wsock-trace. Это DLL, которая встраивается между приложением и библиотекой WinSock, и чтобы воспользоваться трейсером, необходимо при компиляции приложения связать библиотеку с приложением.

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

Лучше всего трейсер работает с MSVC, поскольку код обхода стека требует присутствия PDB-файла с символами.

MinGW не создает PDB-символы, так как использует устаревшую библиотеку BFD.

Трейсер поддерживает функции расширений Microsoft, такие как AcceptEx() и ConnectEx().

Трейсер может учитывать особенности работы сети и, например, имитировать задержки обмена данными:

• Все строки трассировки начинаются с точной временной метки.

• Вызовы приема, передачи, select() и WSAPoll() могут быть замедлены на несколько миллисекунд. Это настраивается в конфигурационном файле.

• Отслеживается активность брандмауэра. Трейсер будет сообщать о действиях, вызывающих события WFP.

Благодаря интеграции с различными сервисами поддерживается множество дополнительных функций:

• Отображение информации о стране, городе и регионе, откуда пришел или куда ушел запрос, благодаря GeoIP.

• Отображение номера автономной системы и данных о ней.

• Поддержка черного списка на основе DNS.

Самая мощная возможность, которую предлагает данный трейсер, — поддержка сценариев Lua, которые могут изменять его поведение во время выполнения.

Диагностика сбоев вне приложения

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

Минимальный перехватчик вызовов

Хотя для решения задачи подойдут разные языки, код перехватчика легче всего реализовать на языке достаточно низкого уровня, таком как C или C++. В данном случае у компилятора с такого языка должна быть возможность формировать таблицу импорта создаваемых библиотек, экспортируя функции по соглашению stdcall.

Приведем пример перехватчика на C++, работающего только в Linux, хотя подобный перехватчик легко реализуется и для Windows по тому же принципу:

// Включить поддержку всех стандартов в библиотеке:

// C89, C99, POSIX, BSD, SVID LFS, X/Open и расширения GNU.

// Макрос влияет на stdio и stdlib.

// Здесь нужен для доступности макроса RTLD_NEXT.

#if !defined(_GNU_SOURCE)

#    define _GNU_SOURCE

#endif

 

extern "C"

{

#include <dlfcn.h>

#include <unistd.h>

}

 

#include <cstdio>

#include <cstdlib>

#include <ctime>

 

// Объявляем, что функция init() должна быть вызвана при загрузке библиотеки

// автоматически.

static void init (void) __attribute__ ((constructor));

 

// Ссылки на функции в C-стиле.

typedef ssize_t (*write_t)(int fd, const void *buf, size_t count);

typedef int (*socket_t)(int domain, int type, int protocol);

typedef int (*close_t)(int fd);

 

static close_t old_close;

static socket_t old_socket;

static write_t old_write;

 

// Дескриптор сокета, пересылка данных через который будет перехвачена.

// В учебных целях будет перехвачено единственное открытие сокета.

static int socket_fd = -1;

static unsigned int seed = std::time(0);

Сохраним оригинальные адреса вызовов:

// В этой функции, которая будет вызвана при загрузке библиотеки,

// сохраняются адреса вызовов.

void init(void)

{

    srand(time(nullptr));

    // Использовать printf(), так как применять STL в C-коде нежелательно,

    // как и смешивать две системы вывода.

    printf("Interceptor library loaded.\n");

 

    // Сохранить указатели на предыдущие вызовы.

    old_close = reinterpret_cast<close_t>(dlsym(RTLD_NEXT, "close"));

    old_write = reinterpret_cast<write_t>(dlsym(RTLD_NEXT, "write"));

    old_socket = reinterpret_cast<socket_t>(dlsym(RTLD_NEXT, "socket"));

}

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

Как минимум необходимо перехватить несколько функций. И обязательно — функцию close():

// Перехват вызовов производится из C-библиотеки, поэтому необходимо выключить

// манглинг C++.

// Если манглинг не будет выключен, придется реализовывать перехватчик

// на чистом C.

extern "C"

{

 

int close(int fd)

{

    if (fd == socket_fd)

    {

        printf("> close() on the socket was called!\n");

        socket_fd = -1;

    }

 

    return old_close(fd);

}

Она сбросит ранее сохраненный дескриптор сокета и вызовет функцию close() из предыдущей библиотеки, и если это LibC, будет выполнен системный вызов.

Для отправки данных функцией send() в Linux используется функция write() для записи в дескриптор. Чтобы исказить данные, необходимо перехватить функцию write():

ssize_t write(int fd, const void *buf, size_t count)

{

    auto char_buf = reinterpret_cast<const char*>(buf);

 

    // Делать что-то, только если это запись в сохраненный дескриптор сокета.

    if (char_buf && (count > 1) && (fd == socket_fd))

    {

        printf("> write() on the socket was called with a string!\n");

        printf("New buffer = [");

 

        for (size_t i = 0; i < count — 1; ++i)

        {

            // С некоторой вероятностью менять произвольные

            // символы буфера на случайные, имитируя "мусор".

            int r = rand_r(&seed);

            char *c = const_cast<char *>(char_buf) + i;

 

            // ASCII-символ.

            if (1 == r % count) *c = r % (0x7f0x20) + 0x20;

 

            putchar(*c);

        }

        printf("]\n");

    }

 

    return old_write(fd, buf, count);

}

Дескриптор socket_fd используется, чтобы проверить, что запись производится именно в сокет, а не, например, в дескриптор ввода или вывода.

После того как данные в буфере были искажены, будет вызван оригинальный write() из библиотеки.

Также перехватим функцию socket(), что нужно для переустановки дескриптора сокета, используемого в перегруженном write():

int socket(int domain, int type, int protocol)

{

    int cur_socket_fd = old_socket(domain, type, protocol);

 

    if (-1 == socket_fd)

    {

        printf("> socket() was called, fd = %d!\n", cur_socket_fd);

        // Первый вызов функции socket(), необходимо сохранить дескриптор.

        socket_fd = cur_socket_fd;

    }

    else

    {

        printf("> socket() was called, but socket was opened already...\n");

    }

 

    return cur_socket_fd;

}

 

} // Конец блока extern "C"

Конечно, перехватчик максимально упрощен и работает только с первым открытым сокетом, предполагая, что другие открыты не будут. Теперь проверим его работу с помощью Netcat.

Внимание! Некоторые версии Netcat используют функцию sendto(), которая может быть реализована не через вызов write(), а через системный вызов sendto(). В таком случае очевидно, что отправка данных не будет перехвачена вследствие того, что не будет сделан вызов write(). Если вы работаете с таким вариантом, перехватите sendto() по аналогии с write() либо используйте другой Netcat или даже написанный ранее UDP-клиент, реализованный через вызов функции send().

Загрузку библиотеки перехватчика выполним через переменную загрузчика LD_PRELOAD, которая предназначена для того, чтобы загружать различные ELF-файлы перед всеми остальными.

Подробнее о загрузчике и его переменных можно прочесть в man 8 ld.so или man 8 ld-linux.so.

Система загрузит нашу библиотеку первой и вызовет функцию init(). На принимающей стороне уже должен прослушивать экземпляр Netcat. Введем там строку Test string и увидим, что она была отправлена серверу:

LD_PRELOAD=build/bin/libb01-ch24-call-intercepter.so nc localhost 11111

Interceptor library loaded.

> socket() was called, fd = 3!

Test string.

> write() on the socket was called with a string!

New buffer = [Test s#ring.]

Перехватчик исказил буфер, и получилась строка «Test s#ring.». Это и видно на принимающей стороне канала:

nc -l -p 11111

Test s#ring.

Напомним, что в данном случае перехватчик нужен только для имитации сложной проблемы.

В реальной ситуации перехватчик, установленный в системе, не пишет сообщений и подключается автоматически, например используя файл /etc/ld.so.preload. Этот файл — аналог механизма AppInit_DLLs в ОС Windows — ветки реестра, содержащей пути к библиотекам, которые загрузчик будет загружать перед запуском любой программы.

Руткиты

Руткиты, установленные, чтобы скрыть действия перехватчиков, могут корректировать содержимое файла ld.so.preload «на лету», а также остаются невидимы для команд просмотра файловой системы. Кроме перехвата данных пользователя, они могут изменять передаваемые данные. Проявлять себя они могут лишь время от времени. Поэтому выявить перехватчик трудно и зачастую получится только на отключенной системе, потому что в работающей системе его просто может не быть на диске. Однако возможности отключить систему может не быть, так как сервер может работать под нагрузкой в непрерывном режиме.

Диагностика проблем сети в канале

Предположим, что вы отладили сервер и клиент приложения и проверили их работоспособность на других машинах. Вы уверены, что ваша программа работает. Однако на одном узле программа устойчиво работает с ошибкой. Можно ли списать все на канал и звать администратора сети? И да и нет.

Нет, потому что до канала есть несколько уровней абстракции, которые также могут работать некорректно.

Да, потому что, если не вы сами установили перехватчик, у вас проблемы. Если ваша машина взломана — это проблема безопасности компании, и необходимо привлекать службу безопасности для расследования инцидента.

Даже если проблема не в перехватчике, а за настройку машины отвечает администратор, эта проблема касается его. Но зачастую разработчик сам является администратором на своей машине и сам занимается настройкой сети. Поэтому до того, как все списать на канал и администратора, стоит попробовать разобраться и провести более глубокую отладку.

И только если проблема не найдена, следует обращаться к администратору, так как это означает, что скорее всего, причина все же в канале.

Обнаружение проблем с вызовами через strace

Если запустить приложение в GDB, поставить точку останова на write() и посмотреть стек вызовов, будет виден перехватчик:

Breakpoint 1, 0x00007ffff7eb18f0 in write () from /usr/lib/libc.so.6

(gdb) bt

#0  0x00007ffff7eb18f0 in write () from /usr/lib/libc.so.6

#1  0x00007ffff7fc1343 in write () from build/bin/libcall-intercepter.so

#2  0x000055555555846d in ?? ()

#3  0x000055555555694c in ?? ()

#4  0x00007ffff7de9b25 in __libc_start_main () from /usr/lib/libc.so.6

#5  0x0000555555556f6e in ?? ()

Но технически перехватчик может искажать стек вызовов.

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

Допустим, первоначальная отладка ничего не дала. Запустим программу, используя утилиту strace:

LD_PRELOAD=build/bin/libb01-ch24-call-intercepter.so strace --trace="%net,write,read" -o log -- nc localhost 11111

Interceptor library loaded.

Interceptor library loaded.

> socket() was called, fd = 3!

Testing.

> write() on the socket was called with a string!

New buffer = [Testi^g.]

Tes buffer

> write() on the socket was called with a string!

New buffer = [Tes buf{er]

^C> close() on the socket was called!

В журнале увидим следующее:

# Создается новый сокет с дескриптором 3.

socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3

 

# Перехватчик информирует об этом, записывая сообщение в stdout

# (дескриптор 1).

write(1, "> socket() was called, fd = 3!\n", 31) = 31

 

# Устанавливаются опции.

setsockopt(3, SOL_SOCKET, SO_LINGER, {l_onoff=1, l_linger=0}, 8) = 0

setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0

 

# Вызывается `connect`.

connect(3, {sa_family=AF_INET, sin_port=htons(11111),

        sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS

 

# EINPROGRESS не ошибка.

getsockopt(3, SOL_SOCKET, SO_ERROR, [0], [4]) = 0

 

# Чтение строки из stdin (дескриптор 0).

read(0, "Testing.\n", 1024)             = 9

 

# Перехватчик выводит на консоль сообщения.

write(1, "> write() on the socket was call"..., 50) = 50

write(1, "New buffer = [Testi^g.]\n", 24) = 24

 

# Запись измененной строки в сокет (дескриптор 3).

write(3, "Testi^g.\n", 9)               = 9

 

read(0, "Tes buffer\n", 1024)           = 11

write(1, "> write() on the socket was call"..., 50) = 50

write(1, "New buffer = [Tes buf{er]\n", 26) = 26

write(3, "Tes buf{er\n", 11)            = 11

 

--- SIGINT {si_signo=SIGINT, si_code=SI_KERNEL} --

 

# Закрытие обоих концов соединения.

shutdown(3, SHUT_RDWR)                  = 0

write(1, "> close() on the socket was call"..., 36) = 36

Видим, что реальный write() будет вызван с уже искаженной строкой. Значит, между вызовом функции из программы и системным вызовом есть перехватчик.

Некоторые опции strace

Опции --trace="%net,write,read" нужны для того, чтобы в лог записывались только вызовы, которые нас интересуют: сетевые, вызовы read и write.

Strace может выполнять множество других задач, например перехватывать вызовы и подменять возвращаемые ими значения, используя опцию -e inject=socket:error=<тип ошибки>.

Опция -k записывает в протокол стек вызовов.

Более подробно ознакомиться с этими возможностями можно в man 1 strace.

Утилиту strace удобно использовать как инструмент первичной диагностики, чтобы сразу видеть, какие системные вызовы и с какими параметрами были выполнены. Также ее можно дополнить утилитами, перечисленными в предыдущей главе.

Например, ltrace показывает библиотечные вызовы, и если использовать эти утилиты совместно, можно делать «срезы» активности приложения на разных уровнях, что дает возможность понять, как осуществляется обмен с внешней средой.

Для ОС Windows свободная утилита Wsock-trace позволяет увидеть вообще все сокетные вызовы сразу, что очень удобно.

Обнаружение проблем с помощью генератора пакетов уровня ядра

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

Отладчики уровня пользователя на Linux тоже не будут работать: они используют ptrace. И остаются только .

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

Ядро должно быть собрано с опцией CONFIG_NET_PKTGEN, которая в большинстве дистрибутивов включена. Генератор собирается как модуль ядра и не загружается автоматически, поэтому сначала нужно загрузить модуль:

sudo modprobe pktgen

После загрузки модуля в /proc/net появится каталог pktgen с файлами, управляющими генератором:

ls /proc/net/pktgen

kpktgend_0  kpktgend_1  kpktgend_2  kpktgend_3  kpktgend_4  kpktgend_5  kpktgend_6  kpktgend_7  pgctrl

 

sudo cat /proc/net/pktgen/pgctrl

Packet Generator for packet performance testing. Version: 2.75

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

Например, мы хотим использовать генератор 5:

sudo cat /proc/net/pktgen/kpktgend_5

Running:

Stopped:

Result: NA

Настроим генератор на требуемый интерфейс:

sudo bash -c 'echo "add_device lo" > /proc/net/pktgen/kpktgend_5'

Локальная петля была использована в учебных целях.

Как вы заметили, используется запись вида sudo bash -c 'echo ... > /dev/net/pktgen/...'.

Это сделано для того, чтобы можно было перенаправить вывод команды echo в файл с правами суперпользователя.

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

sudo -i.

su -.

либо просто выполнив логин на одном из псевдотерминалов, если вы не используете GUI.

Теперь можно проверить канал через данный интерфейс:

sudo cat /proc/net/pktgen/kpktgend_5

Running:

Stopped: lo

Result: OK: add_device=lo

В каталоге pktgen появился новый файл:

sudo cat /proc/net/pktgen/lo

Params: count 1000  min_pkt_size: 0  max_pkt_size: 0

    frags: 0  delay: 0  clone_skb: 0  ifname: lo

    flows: 0 flowlen: 0

    queue_map_min: 0  queue_map_max: 0

    dst_min:   dst_max:

    src_min:   src_max:

    src_mac: 00:00:00:00:00:00 dst_mac: 00:00:00:00:00:00

    udp_src_min: 9  udp_src_max: 9  udp_dst_min: 9  udp_dst_max: 9

    src_mac_count: 0  dst_mac_count: 0

    Flags:

Current:

    pkts-sofar: 0  errors: 0

    started: 0us  stopped: 0us idle: 0us

    seq_num: 0  cur_dst_mac_offset: 0  cur_src_mac_offset: 0

    cur_saddr: 0.0.0.0  cur_daddr: 0.0.0.0

    cur_udp_dst: 0  cur_udp_src: 0

    cur_queue_map: 0

    flows: 0

Result: Idle

Настраиваем генератор на интерфейсе. Задаем адреса, количество пакетов и прочие опциональные настройки:

sudo bash -c "echo 'dst 127.0.0.1' > /proc/net/pktgen/lo"

sudo bash -c "echo 'count 50' > /proc/net/pktgen/lo"

sudo bash -c "echo 'delay 50' > /proc/net/pktgen/lo"

sudo bash -c "echo 'pkt_size 1400' > /proc/net/pktgen/lo"

sudo bash -c "echo 'udp_dst_min 11100' > /proc/net/pktgen/lo"

sudo bash -c "echo 'udp_dst_max 11150' > /proc/net/pktgen/lo"

Проверяем результат:

sudo cat /proc/net/pktgen/lo

Params: count 50  min_pkt_size: 1400  max_pkt_size: 1400

    frags: 0  delay: 50  clone_skb: 0  ifname: lo

    flows: 0 flowlen: 0

    queue_map_min: 0  queue_map_max: 0

    dst_min: 127.0.0.1  dst_max:

    src_min:   src_max:

    src_mac: 00:00:00:00:00:00 dst_mac: 00:00:00:00:00:00

    udp_src_min: 9  udp_src_max: 9  udp_dst_min: 11100  udp_dst_max: 11150

    src_mac_count: 0  dst_mac_count: 0

    Flags:

Current:

    pkts-sofar: 0  errors: 0

    started: 0us  stopped: 0us idle: 0us

    seq_num: 0  cur_dst_mac_offset: 0  cur_src_mac_offset: 0

    cur_saddr: 0.0.0.0  cur_daddr: 127.0.0.1

    cur_udp_dst: 11150  cur_udp_src: 0

    cur_queue_map: 0

    flows: 0

Result: OK: udp_dst_max=11150

Были заданы порты из диапазона, на котором работало приложение. Часто есть смысл проверить также работу портов с номерами до 1024.

Наконец, запустим генератор:

sudo bash -c 'echo start > /proc/net/pktgen/pgctrl'

Результат:

sudo cat /proc/net/pktgen/lo

Params: count 50  min_pkt_size: 1400  max_pkt_size: 1400

    frags: 0  delay: 50  clone_skb: 0  ifname: lo

    flows: 0 flowlen: 0

    queue_map_min: 0  queue_map_max: 0

    dst_min: 127.0.0.1  dst_max:

    src_min:   src_max:

    src_mac: 00:00:00:00:00:00 dst_mac: 00:00:00:00:00:00

    udp_src_min: 9  udp_src_max: 9  udp_dst_min: 11100  udp_dst_max: 11150

    src_mac_count: 0  dst_mac_count: 0

    Flags:

Current:

    pkts-sofar: 50  errors: 0

    started: 1887512514145us  stopped: 1887512514494us idle: 0us

    seq_num: 51  cur_dst_mac_offset: 0  cur_src_mac_offset: 0

    cur_saddr: 127.0.0.1  cur_daddr: 127.0.0.1

    cur_udp_dst: 11100  cur_udp_src: 9

    cur_queue_map: 0

    flows: 0

Result: OK: 348(c348+d0) usec, 50 (1400byte,0frags)

143333pps 1605Mb/sec (1605329600bps) errors: 0

Посмотрим на рис. 24.5. В Wireshark будут видны сгенерированные пакеты:

Рис. 24.5. Сгенерированные пакеты в Wireshark

Данные пакетов нулевые — можно предположить, что искажений в канале нет.

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

sudo bash -c 'echo stop > /proc/net/pktgen/pgctrl'

sudo bash -c 'echo rem_device_all > /proc/net/pktgen/kpktgend_5'

В примере остановка не обязательна, так как будет сгенерировано фиксированное количество пакетов, и после этого генератор остановится сам.

Следующая команда сбрасывает все генераторы и удаляет связанные устройства:

sudo bash -c 'echo reset > /proc/net/pktgen/pgctrl'

По окончании работы с генератором выгрузим модуль:

sudo bash -c 'modprobe -r pktgen'

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

По крайней мере до версии 6 ядра включительно генератор не позволял задавать шаблон данных пакета.

Упомянутые в главе 23 фреймворки для kernel bypass эту возможность имеют. Можно использовать генераторы из них. Генератор Pktgen-DPDK вообще позволяет выполнять скрипты на Lua, управляя процессом генерации в зависимости от произвольных условий. В составе кросс-платформенного фреймворка тоже есть свой генератор.

Для Windows, за исключением DPDK, есть по крайней мере одно решение уровня ядра — . Более полный список генераторов пакетов можно посмотреть , и .

Scapy

Scapy — очень удобный Python-фреймворк, который можно использовать как интерактивный инструмент или подключать как модуль в свое приложение. На рис. 24.6 показано окно консоли Scapy после запуска.

Рис. 24.6. Окно запуска Scapy

Scapy используется для тестирования безопасности сети и позволяет строить PDU для разных протоколов, которые можно в дальнейшем отправлять и получать ответ.

Например, простейший вариант ping, реализованный прямо в интерактивной оболочке:

sudo scapy -H

Welcome to Scapy (2.5.0)

>>> p = IP(dst='github.com') / ICMP()

>>> res = sr1(p)

Begin emission:

Finished sending 1 packets.

 

Received 19 packets, got 1 answers, remaining 0 packets

>>> res

<IP  version=4 ihl=5 tos=0x0 len=28 id=18549 flags= frag=0 ttl=54 proto=icmp chksum=0x7460 src=140.82.121.4 dst=192.168.2.13 |<ICMP  type=echo-reply code=0 chksum=0x0 id=0x0 seq=0x0 |>>

В этом примере мы построили IP-пакет с адресом назначения github.com. Scapy выполнит разрешение имен самостоятельно. В пакет мы поместили ICMP-сообщение. По умолчанию это Echo Request. Затем, используя функцию sr1(), мы отправили данный пакет и получили ответ. В сниффере это будет выглядеть, как показано на рис. 24.7.

Рис. 24.7. Дамп работы ping в Scapy

Из примера видно, что Scapy берет на себя множество рутинных операций, к тому же предлагает все имеющиеся возможности языка Python. Например, чтобы получить информацию о функции sr1(), можно использовать функцию help().

Help on function sr1 in module scapy.sendrecv:

 

sr1(x, promisc=None, filter=None, iface=None, nofilter=0, *args, **kargs)

   Send packets at layer 3 and return only the first answer

  

   :param pks: SuperSocket instance to send/receive packets

   :param pkt: the packet to send

   :param rcv_pks: if set, will be used instead of pks to receive packets.

       packets will still be sent through pks

   :param nofilter: put 1 to avoid use of BPF filters

   :param retry: if positive, how many times to resend unanswered packets

       if negative, how many times to retry when no more packets

       are answered

   :param timeout: how much time to wait after the last packet has been sent

   :param verbose: set verbosity level

   :param multi: whether to accept multiple answers for the same stimulus

   :param prebuild: pre-build the packets before starting to send them.

       Automatically enabled when a generator is passed as the packet

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

Существует также , реализованный на C. Он представляет собой утилиту, которая может собирать пакеты. Эта утилита основана на библиотеке , используемой еще несколькими инструментами, например Ettercap.

Поэтому разработчику данный фреймворк тоже может быть полезен. Причем не только для тестирования особых случаев, но и для реализации сервисных программ, как пример — для работы с TUN/TAP-интерфейсами в главе 23.

Подробнее мы рассмотрим Scapy и другие схожие решения в книге 3, в главах, посвященных безопасности.

Резюме

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

Обнаружение ошибок начинается с локализации места их возникновения: приложение это или канал, и иногда требуется не исправлять «ошибки» приложения, а обратиться к сетевому администратору и начать исправление ошибок в сети.

В поиске ошибок помогают средства встроенной диагностики ОС, включая такие «обычные» утилиты, как ping или netstat. Конечно, есть и более сложные утилиты для трассировки, работы с доменными именами, настройки сети. Таких утилит достаточно много.

Для анализа данных, отправленных приложением либо пришедших к нему, полезны сетевые анализаторы, или снифферы, которые мы рассмотрели в главе 23.

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

Чтобы найти «сложные ошибки», могут быть использованы средства трассировки. Они полезны не только при отладке, но и при оптимизации. К таким инструментам относятся BPF Compiler Collection или BPF toolkit, tcptrace, iptables-tracer, nsntrace. Для отладки WinSock существует хорошая утилита Wsock-trace.

Утилита «общего плана» — strace — позволяет увидеть системные вызовы, которые выполняет приложение, а также имеет фильтры, позволяющие ей отображать только вызовы, относящиеся к сети.

Если необходимо исключить проблемы, возникающие только в пользовательском пространстве, можно использовать средства ядра, например встроенный генератор пакетов Linux.

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

Вопросы и задания

1. Какие основные заблуждения существуют у разработчиков относительно сети и распределенных систем? Каковы последствия этих заблуждений?

2. Где необходимо искать проблемы, когда сетевое приложение не работает?

3. Что обычно является первым шагом в диагностике проблем сетевых приложений?

4. Всегда ли разработчик должен решать сетевые проблемы самостоятельно?

5. Какие инструменты и методы можно использовать для обнаружения ошибок?

6. Как может помочь в диагностике утилита Ping?

7. Как явно потребовать от ping отправки пакетов IPv4, если вызов ping с доменным именем узла использует IPv6?

8. Чем может быть полезна утилита SS? Чем она различается с Netstat?

9. Какие утилиты позволяют работать с доменными именами, выполняя запросы к DNS? Как они могут помочь разработчику?

10. Как называется основная утилита для управления сетью в Linux?

11. Используя какую утилиту можно управлять сетью из командной строки в ОС Windows?

12. Какие проблемы может вызывать межсетевой экран?

13. В чем заключается роль сетевых анализаторов, или снифферов, при разработке сетевых приложений?

14. Перечислите несколько полезных утилит из проекта BCC. Чем они могут помочь при разработке?

15. Какие утилиты и инструменты могут помочь отслеживать события при работе соединений в сетевых приложениях?

16. Каким способом перехватывает вызовы разработанный нами перехватчик? Можно ли организовать перехват на уровне системы?

17. Почему реализованный нами перехватчик может не работать с некоторыми реализациями Netcat? Как это исправить?

18. Как утилита strace позволяет выявить перехватчик? Для чего она еще может быть полезна?

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

20. Что такое Scapy? Как она может помочь разработчику сетевых приложений?

21. Как в Scapy сформировать и отправить пакет? А как получить ответ?

22. Изучите, как работает traceroute в вашей ОС. Реализуйте ICMP traceroute, переделав в него одну из утилит ping, реализованных в предыдущих главах.

23. Доработайте перехватчик так, чтобы он не искажал передаваемые данные, а записывал их в файл.

24. Доработайте перехватчик так, чтобы на сервер были отправлены:

a) IP-адрес и порт, используемые сокетом;

б) строки, которые начинаются с USER и PASS;

в) факт закрытия сокета.

25. Установите FTP-сервер, настройте авторизацию, подключитесь с помощью FTP-клиента в пассивном режиме и попробуйте сделать следующее:

a) обнаружить логин и пароль с помощью tcpdump;

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


Назад: Глава 23. TUN/TAP-интерфейсы. Техника Kernel bypass
Дальше: Глава 25. Поиск ошибок и обнаружение места сбоя