Анатомия сетевого стека linux

Анатомия сетевого стека linux

Исследование протоколов — довольно интересное занятие. По-прежнему многие исследователи занимаются их оценкой и верификацией в различных ситуациях. Лично я столкнулся с необходимостью исследования протокола в первый год работы в ИММ УрО РАН [1]. Как бы это ни звучало банально, но это был TCP протокол. Причина, по которой я стал этим заниматься, следующая — необходимо было разобраться с сетью в одном из вычислительных кластеров. При передаче данных по протоколу TCP через 2-е 100 Мбит. сетевые карты одновременно (т.н. связывание каналов или по англ., bonding) наблюдался прирост скорости в два раза, и в тоже время на гигабитных карточках скорость не только не возрастала, но даже падала до 600 Мбит. в сек. Снифер ничего особого не показал. Да и вообще говоря, снифер не всегда может дать исчерпывающую информацию о происходящем в сети. С его помощью можно узнать о том, что произошло в сети и на основе этого попытаться понять, что происходило с протоколом. Но для этого как минимум необходимо точно себе представлять реализацию данного протокола в этой ОС. Согласитесь, это не легко. Да и надо ли это вообще, когда сама ОС и все протоколы прямо перед нами? К тому же, когда это open-source ОС, где нет ничего, что было бы скрыто от наших глаз.

Данная статья описывает один из двух (на данный момент мне известных!) способов встраивания в TCP/IP стек. Я это делал с целью узнать значение некоторых параметров и состояние протокольной машины TCP для конкретного соединения. Возможно, у вас будут на это другие причины :). Первый способ — это внести изменения в ядро ОС Linux, пересобрать его, перезагрузить машину и затем собирать необходимую информацию. И если пойти дальше, то можно даже включить это опцией при сборке ядра. Но этот способ примитивный, скучный, а значит, не доставляет никакого удовольствия. Второй — написать модуль к ядру, который бы подключался к TCP протоколу и выводил всю необходимую информацию. Он немного сложнее первого, но значительно интереснее, поверьте! Вот его мы и разберем.

В связи с тем, что изменения в ядро ОС Linux вносятся довольно часто, трудно написать пример кода, который бы работал на многих ядрах сразу. По этой причине, в этой статье описан пример встраивания в TCP стек в ядро ОС Linux версии 2.6.12. Также, в приложении можно найти код для версии ядра 2.6.18 (спасибо VenROCK’у, заставил!).

Сетевая подсистема ОС GNU/Linux

Начнем, пожалуй, с того, как в ОС Linux представлен сетевой стек. Для работы с сетью ОС Linux в основном используется 2-е структуры. Первая — это sk_buff, описывает пакет с содержимым и хранит служебную информацию, например, для какого сокета адресован данный пакет, когда он был создан, указатели на заголовки (Ethernet, IP, TCP..), размер пакета, с какого интерфейса пакет был получен или через какой его необходимо отправить и т.д. Некоторые из этих полей задаются в момент создания пакета, другие же — в процессе. Например, при отправке пакета структура sk_buff изначально содержит ссылку на сокет, а при получении пакета из сети — NULL, и это поле заполняется в процессе. Правда, некоторые поля могут оставаться пустыми на протяжении всего процесса существования, в зависимости от протокола.

Как только у ОС появляется необходимость работать с сетевым пакетом — это sk_buff. Она создается в тот момент, когда система получила пакет из сети, или когда ей необходимо сформировать новый для отправки. По этой причине в сетевом стеке практически каждая функция получает эту структуру в качестве параметра. Основное её назначение — дать простой и эффективный способ работы с пакетом на всех сетевых уровнях сетевого стека (см. рис. 1). Структура sk_buff представляет собой управляющую структуру с присоединенным блоком памяти, в котором находится пакет [2]. Таким образом, изменяя переменные в структуре, мы изменяем содержимое пакета или служебную информацию о нем.

Ниже представлена основная часть этой структуры.

Более подробное её описание можно найти в исходных текстах ядра Linux: include/linux/skbuff.h.

Вторая структура, с которой необходимо познакомится — это sock, содержит информацию о состоянии сокета (connected, unconnected…), его тип (SOCK_STREAM, SOCK_DGRAM, SOCK_RAW), используемый протокол (TCP, UDP, IPPROTO_RAW …), размер буфера для приема и отправки пакетов, указатели на область памяти, где расположены буферы для приема и отправки пакетов, и т.д. Она создается и заполняется в момент, когда пользователь делает системный вызов socket. Ниже представлен кусок этой структуры с некоторыми комментариями.

Её полное описание можно найти в include/net/sock.h.

Имея в своем распоряжении эти две структуры (struct sk_buff и struct sock), можно принимать и отправлять пакеты, доставлять информацию от сетевого интерфейса к пользовательскому приложению и наоборот. Но структура sock описывает только общие вещи. Например, она не содержит ожидаемый номер последовательности TCP сегмента и многих других полезных вещей. Для этих целей служат другие структуры.

Читайте также:  Citrix workspace для windows 32 bit

Каждый протокол имеет свою структуру для хранения необходимой ему информации. Например, для TCP эта структура называется tcp_sock, для UDP — udp_sock, продолжите сами :). Все они наследуют структуру sock, правда, не всегда напрямую, как это в случае с tcp_sock (см. рис 2). Все самое интересное о работе протокола можно найти в этих структурах — его состояние, таймауты, номер последовательности ожидаемого пакета, и много другой специфической информации. Таким образом, самый лучший способ узнать, что происходит с протокольной машиной TCP — это считывать данные из структуры tcp_sock.

Доступ к структуре tcp_sock

Перед тем как начать поиск, необходимо выяснить, что именно мы хотим обнаружить? Как было описано выше, tcp_sock наследует структуру sock, вот почему сначала нам необходимо найти нужный сокет, а по нему мы сможем получить доступ к tcp_sock. В ОС Linux для быстрого поиска TCP и UDP сокета их хранят в специальной хеш таблице. Место их расположения определяется путем вычисления некоторого хеша на основе 4-х параметров: адрес отправителя, порт отправителя, адрес получателя и порт получателя. Ну что же, логично! После определения его места расположения сокет можно извлечь. В самом TCP протоколе операция по обнаружению и извлечению сокета описана в функции __tcp_v4_lookup_established, которая находится в файле net/ipv4/tcp_ipv4.c. Вот её исходный код:

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

А затем извлекается сам сокет:

Функция, которая производит расчет хеша, называется tcp_hashfn, начиная с версии ядра 2.6.18 она стала называться inet_ehashfn() и её перенесли в заголовочный файл, так что теперь нет нужды описывать её в своем коде. Но мы пишем код под 2.6.12, так что придется нам посмотреть, где и как описывается эта функция. Исходный код её так же можно подсмотреть в net/ipv4/tcp_ipv4.c.

Тут стоит сделать одно важное замечание — локальный порт передается в обычном виде (LittleEndian), а порт назначения в сетевом порядке байт (BigEndian), т.е. необходимо выполнить htons(dest). После извлечения сокета доступ к структуре tcp_sock производится очень просто:

Ну а дальше, что вашей душе угодно.

В приложении можно найти исходный код для ядра 2.6.12 и 2.6.18. Используя NetFilter[4], модуль перехватывает все входящие пакеты и анализирует их. Если этот пакет относится к искомому соединению, тогда мы вычисляем хеш, получаем указатель на сокет, извлекаем его, и через него получаем доступ к tcp_sock. Если не понятно, читайте все сначала :).

Примечание

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

Автор выражает благодарность за помощь при подготовке данной статьи Игумнову А.С. и Шарфу С.В. из ИММ УрО РАН.

Источник

Для системного администратора

Анатомия сетевого стека в Linux

Одна из величайших возможностей операционной системы Linux® — ее сетевой стек. Он является производной стека BSD и хорошо оснащен добротным набором интерфейсов, которые варьируются от протоколо-независимых (protocol agnostic), таких как интерфейс уровня общих сокетов или уровня устройств, до специальных интерфейсов конкретных сетевых протоколов. В этой статье исследуется структура сетевого стека Linux с точки зрения его уровней, а также рассматриваются некоторые из основных структур.

Введение в протоколы

В то время как формальное введение в работу в сети отсылает нас к модели взаимодействия открытых систем (OSI — Open Systems Interconnection), это введение в основной сетевой стек в Linux использует четырехуровневую модель, известную как модель Интернет (Internet model) (смотрите рисунок 1).

Рисунок 1. Интернет-модель сетевого стека

Внизу стека располагается канальный уровень. Канальный уровень относится к драйверам устройств, обеспечивающим доступ к физическому уровню, который может состоять из многочисленных сред, таких как последовательные каналы или устройства Ethernet. Над канальным находится сетевой уровень, который отвечает за направление пакетов по назначению. Следующий уровень под названием транспортный отвечает за одноранговые (peer-to-peer) коммуникации (например, в пределах хоста). Сетевой уровень управляет связью между хостами, а транспортный — взаимодействием между конечными точками внутри этих хостов. Наконец, существует прикладной уровень, который обычно является семантическим и понимает перемещенные данные. К примеру, протокол передачи гипертекста (HTTP — Hypertext Transfer Protocol) перемещает запросы и ответы для содержимого Web между сервером и клиентом.

В сущности, уровни сетевого стека проходят под более узнаваемыми названиями. На канальном уровне вы найдете Ethernet, наиболее распространенную высокоскоростную среду. К более старым протоколам канального уровня относятся такие последовательные протоколы, как Internet-протокол для последовательной линии (SLIP — Serial Line Internet Protocol), Compressed SLIP (CSLIP) и Point-to-Point Protocol (PPP). Наиболее распространенным протоколом сетевого уровня является Internet Protocol (IP), но существуют и другие, которые удовлетворяют другим нуждам, такие как Протокол управляющих сообщений Internet (ICMP — Internet Control Message Protocol) и Протокол разрешения адресов (ARP — Address Resolution Protocol). На транспортном уровне это Протокол управления передачей (TCP — Transmission Control Protocol) и Протокол пользовательских датаграмм (UDP — User Datagram Protocol). Наконец, прикладной уровень включает в себя множество привычных нам протоколов, в том числе HTTP, стандартный Web-протокол, и SMTP (Simple Mail Transfer Protocol), протокол передачи электронной почты.

Читайте также:  What is microsoft windows 98se

Архитектура базовой сети

Теперь перейдем к архитектуре сетевого стека Linux и посмотрим, как он реализует модель Internet. На рисунке 2 представлен высокоуровневый вид сетевого стека Linux. Наверху располагается уровень пользовательского пространства или прикладной уровень, который определяет пользователей сетевого стека. Внизу находятся физические устройства, которые обеспечивают возможность соединения с сетями (последовательные или высокоскоростные сети, как Ethernet). В центре, или в пространстве ядра, — сетевая подсистема, которая находится в центре внимания данной статьи. Через внутреннюю часть сетевого стека проходят буферы сокетов (sk_buffs), которые перемещают данные пакета между источниками и получателями. Кратко будет показана структура sk_buff.


Рисунок 2. Высокоуровневая архитектура сетевого стека Linux

Во-первых, вам предлагается краткий обзор основных элементов сетевой подсистемы Linux с подробностями в следующих разделах. Наверху (смотрите рисунок 2) находится система под названием интерфейс системного вызова. Она просто дает способ приложениям из пользовательского пространства получать доступ к сетевой подсистеме ядра. Следующим идет протоколо-независимый (protocol agnostic) уровень, который предоставляет общий способ работы с нижестоящими протоколами транспортного уровня. Дальше следуют фактические протоколы, к которым в системе Linux относятся встроенные протоколы TCP, UDP и, конечно же, IP. Следующий — еще один независимый уровень, который обеспечивает общий интерфейс к отдельным доступным драйверам устройств и от них, сопровождаемый в конце самими этими драйверами.

Интерфейс системного вызова

Интерфейс системного вызова может быть описан в двух ракурсах. Когда сетевой вызов производится пользователем, он мультиплексируется через системный вызов в ядро. Это заканчивается как вызов sys_socketcall в ./net/socket.c, который потом демультиплексирует вызов намеченной цели. Другой ракурс интерфейса системного вызова — использование нормальных файловых операций для сетевого ввода/вывода (I/O). Например, обычные операции чтения и записи могут быть выполнены на сетевом сокете (который представляется файловым дескриптором как нормальный файл). Поэтому пока существуют операции, специфичные для работы в сети (создание сокета вызовом socket, связывание его с дескриптором вызовом connect и так далее), есть также и некоторое количество стандартных файловых операций, которые применяются к сетевым объектам, как к обычным файлам. Наконец, интерфейс системного вызова предоставляет средства для передачи управления между приложением в пользовательском пространстве и ядром.

Протоколо-независимый интерфейс (Protocol agnostic interface)

Уровень сокетов является протоколо-независимым (protocol agnostic) интерфейсом, который предоставляет набор стандартных функций для поддержки ряда различных протоколов. Этот уровень не только поддерживает обычные TCP- и UDP-протоколы, но также и IP, raw Ethernet и другие транспортные протоколы, такие как Протокол управления передачей потоков данных (SCTP — Stream Control Transmission Protocol).

Взаимодействие через сетевой стек происходит посредством сокета. Структура сокета в Linux — struct sock, определенная в linux/include/net/sock.h. Эта большая структура содержит все необходимые состояния отдельного сокета, включая определенный протокол, используемый сокетом, и операции, которые можно над ним совершать.

Сетевая подсистема знает о доступных протоколах из специальной структуры, которая определяет ее возможности. Каждый протокол содержит структуру под названием proto (она находится в linux/include/net/sock.h). Эта структура определяет отдельные операции сокета, которые могут выполняться из уровня сокетов на транспортный уровень (например, как создать сокет, как установить соединение с сокетом, как закрыть сокет и т.д.).

Сетевые протоколы

Раздел сетевых протоколов определяет отдельные доступные сетевые протоколы (такие как TCP, UDP и так далее). Они инициализируются в начале дня в функции inet_init в linux/net/ipv4/af_inet.c (так как TCP и UDP относятся к семейству протоколов inet). Функция inet_init регистрирует каждый из встроенных протоколов, использующих функцию proto_register. Эта функция определена в linux/net/core/sock.c, и кроме добавления протокола в список действующих, если требуется, может выделять один или более slab-кэшей.

Можно увидеть, как отдельные протоколы идентифицируют сами себя посредством структуры proto в файлах tcp_ipv4.c, udp.c и raw.c, в linux/net/ipv4/. Каждая из этих структур протоколов отображается в виде типа и протокола в inetsw_array, который приписывает встроенные протоколы их операциям. Структура inetsw_array и его связи показаны на рисунке 3. Каждый из протоколов в этом массиве инициализируется в начале дня в inetsw вызовом inet_register_protosw из inet_init. Функция inet_init также инициализирует различные модули inet, такие как ARP, ICMP, IP-модули и TCP и UDP-модули.

Рисунок 3. Структура массива Internet-протокола

Обратите внимание на рисунке 3, что структура proto определяет транспортные методы сокета, в то время как структура proto_ops — общие. Дополнительные протоколы можно добавить в переключатель протоколов inetsw с помощью вызова inet_register_protosw. Например, SCTP добавляет себя вызовом sctp_init в linux/net/sctp/protocol.c.

Читайте также:  Idle для python linux

Перемещение данных для сокетов происходит при помощи основной структуры под названием буфер сокета (sk_buff). В sk_buff содержатся данные пакета и данные о состоянии, которые охватывают несколько уровней стека протокола. Каждый отправленный или полученный пакет представлен в sk_buff. Структура sk_buff определяется в linux/include/linux/skbuff.h и показана на рисунке 4.

Рисунок 4. Буфер сокета и его связи с другими структурами

Как можно заметить, несколько структур sk_buff для данного соединения могут быть связаны вместе. Каждая из них идентифицирует структуру устройства (net_device), которому пакет посылается или от которого получен. Так как каждый пакет представлен в sk_buff, заголовки пакетов удобно определены набором указателей (th, iph и mac для Управления доступом к среде (заголовок Media Access Control или MAC). Поскольку структуры sk_buff являются центральными в организации данных сокета, для управления ими был создан ряд функций поддержки. Существуют функции для создания, разрушения, клонирования и управления очередностью sk_buff.

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


Устройство-независимый интерфейс (Device agnostic interface)

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

Прежде всего, драйверы устройств могут регистрировать и разрегистрировать себя в ядре вызовом register_netdevice или unregister_netdevice. Вызывающая команда сначала заполняет структуру net_device, а затем передает ее для регистрации. Ядро вызывает свою функцию init (если она определена), выполняет несколько проверок исправности, создает запись sysfs и потом добавляет новое устройство в список устройств (связанный список устройств, активных в ядре). Структуру net_device можно найти в linux/include/linux/netdevice.h. Некоторые функции находятся в linux/net/core/dev.c.

Для отправления sk_buff из уровня протокола устройству используется функция dev_queue_xmit. Она ставит в очередь sk_buff для возможной пересылки соответствующим драйвером устройства (устройством, определенным при помощи net_device или указателя sk_buff->dev в sk_buff). Структура dev содержит метод под названием hard_start_xmit, который хранит функцию драйвера для инициализации передачи sk_buff.

Получение пакета выполняется традиционно при помощи netif_rx. Когда драйвер устройства более низкого уровня получает пакет (содержащийся внутри выделенного sk_buff), sk_buff идет выше, на сетевой уровень, с помощью вызова netif_rx. Эта функция затем ставит sk_buff в очередь на более высокий уровень протоколов для дальнейшей обработки при помощи netif_rx_schedule. Функции dev_queue_xmit и netif_rx находятся в linux/net/core/dev.c.

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

Драйверы устройств

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

Во время инициализации драйвер устройства выделяет место для структуры net_device, а затем инициализирует ее необходимыми подпрограммами. Одна из них, с названием dev->hard_start_xmit, определяет, как верхний уровень должен поставить в очередь sk_buff для передачи. Ей передается sk_buff. Работа этой функции зависит от оборудования, но обычно пакет, описываемый в sk_buff, перемещается в так называемое “аппаратное кольцо” (hardware ring) или “очередь” (queue). Поступление кадра, как описано на устройство-независимом уровне, использует интерфейс netif_rx или netif_receive_skb для NAPI-совместимого сетевого драйвера. Драйвер NAPI накладывает ограничения на возможности базового оборудования.

После того как драйвер устройства настроил свои интерфейсы в структуре dev, вызов register_netdevice делает ее доступной для использования. В linux/drivers/net можно найти драйверы, характерные для сетевых устройств.

Идем дальше

Исходный код Linux — прекрасный способ узнать о конструкции драйверов для множества типов устройств, включая драйверы сетевых устройств. Вы обнаружите различия в конструкции и использовании доступных API ядра, но каждый будет полезен либо инструкциями, либо как отправная точка для нового драйвера. Остальной код в сетевом стеке стандартен и используется, пока не потребуется новый протокол. Но даже тогда реализации TCP (для потокового протокола) или UDP (для протокола на основе передачи сообщений) служат полезными моделями для начала новой разработки.

Автор: М. Тим Джонс, инженер-консультант, Emulex
Взято с ibm developerworks

Источник

Оцените статью