XDP или Express Data Path возникает из-за острой необходимости высокопроизводительной обработки пакетов в ядре Linux. Несколько методов обхода ядра (наиболее известный из которых - DPDK) направлены на ускорение сетевых операций за счет перемещения обработки пакетов в пространство пользователя.

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

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

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

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

Если вы пропустили мою предыдущую запись в блоге о Основах eBPF, я рекомендую вам сначала прочитать ее. Чтобы выделить положение XDP в сетевом стеке, давайте посмотрим, каков срок службы TCP-пакета с момента его поступления на сетевую карту до тех пор, пока он не попадет в целевой сокет в пользовательском пространстве. Имейте в виду, что это будет общий обзор. Мы просто коснемся этого сложного зверя, которым является сетевой стек ядра.

Входящий поток пакетов через сетевой стек

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

Поскольку путь выполнения драйвера должен быть невероятно быстрым, обработка откладывается вне контекста IRQ драйвера посредством мягких IRQ (NET_RX_SOFTIRQ). Учитывая, что IRQ отключены во время выполнения обработчика прерывания, ядро ​​предпочитает планировать длительные задачи вне контекста IRQ, чтобы избежать потери любых событий, которые могут произойти, пока процедура прерывания занята. Драйвер устройства запускает цикл NAPI, и потоки ядра для каждого процессора (ksoftirqd) потребляют пакеты из кольцевого буфера. Ответственность за цикл NAPI в первую очередь связана с запуском мягких IRQ (NET_RX_SOFTIRQ) для обработки обработчиком softirq, который, в свою очередь, отправляет данные в сетевой стек.

Драйвер сетевого устройства выделяет новый буфер сокета (sk_buff) для обработки потока входящих пакетов. Буфер сокета представляет собой фундаментальную структуру данных для абстрагирования буферизации пакетов / манипуляции с ними в ядре. Он также поддерживает все верхние уровни сетевого стека.

В структуре буфера сокета есть несколько полей, которые определяют разные сетевые уровни. После использования буферных сокетов из очередей ЦП ядро ​​заполняет метаданные, клонирует sk_buff и отправляет их вверх по течению на последующие сетевые уровни для дальнейшей обработки. Здесь в стеке регистрируется уровень протокола IP. Уровень IP выполняет некоторые базовые проверки целостности и передает пакет перехватчикам netfilter. Если пакет не отброшен сетевым фильтром, уровень IP проверяет протокол высокого уровня и передает обработку функции обработчика для ранее извлеченного протокола.

В конечном итоге данные копируются в буферы пользовательского пространства, к которым подключены сокеты. Процессы получают данные либо через семейство блокирующих системных вызовов (recv, read), либо проактивно через какой-либо механизм опроса (epoll).

Перехватчики XDP запускаются сразу после того, как сетевой адаптер копирует пакетные данные в очередь RX, и в этот момент мы можем эффективно предотвратить выделение различных структур метаданных, включая sk_buffers. Если мы рассмотрим простейший возможный вариант использования, такой как фильтрация пакетов в высокоскоростных сетях или узлах, которые подвергаются DDoS-атакам, традиционные сетевые брандмауэры (iptables) неизбежно затопят машину из-за количества рабочая нагрузка, вносимая каждым этапом сетевого стека.

В частности, правила iptables, которые запланированы в их собственных задачах softirq, но также оцениваются последовательно, будут совпадать на уровне протокола IP, чтобы решить, является ли пакет с определенного IP-адреса вот-вот будет сброшен. Напротив, XDP будет напрямую работать с необработанным кадром Ethernet, полученным из кольцевого буфера с поддержкой DMA, поэтому логика отбрасывания может произойти преждевременно, избавляя ядро ​​от огромной обработки, которая приведет к задержке сетевого стека и, в конечном итоге, к полному нехватке ресурсов.

Конструкции XDP

Как вы, возможно, уже знаете, байт-код eBPF может быть прикреплен к различным стратегическим точкам, таким как функции ядра, сокеты, точки трассировки, иерархии групп или символы пользовательского пространства. Таким образом, каждый тип программы eBPF работает в определенном контексте - состоянии регистров ЦП в случае kprobes, буферов сокетов для программ сокетов и так далее. Выражаясь языком XDP, основа результирующего байт-кода eBPF моделируется на основе контекста метаданных XDP (xdp_md). Контекст XDP содержит все необходимые данные для доступа к пакету в его необработанном виде.

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

#include <linux/bpf.h> 
#define SEC(NAME) __attribute__((section(NAME), used)) 
SEC("prog") 
int xdp_drop(struct xdp_md *ctx) { 
    return XDP_DROP; 
} 
char __license[] SEC("license") = "GPL";

Это минимальная программа XDP, которая после подключения к сетевому интерфейсу отбрасывает каждый пакет. Начнем с импорта заголовка bpf, который вводит определения различных структур, включая структуру xdp_md. Затем мы объявляем макрос SEC для размещения карт, функций, метаданных лицензий и других элементов в разделах ELF, которые подвергаются интроспекции загрузчиком eBPF.

Теперь наступает самая важная часть нашей программы XDP, которая имеет дело с логикой обработки пакетов. XDP поставляется с предопределенным набором вердиктов, которые определяют, как ядро ​​перенаправляет поток пакетов. Например, мы можем передать пакет в обычный сетевой стек, отбросить его, перенаправить пакет на другой сетевой адаптер и т. Д. В нашем случае XDP_DROP дает сверхбыстрое отбрасывание пакетов. Также обратите внимание, что мы закрепили раздел prog в нашей функции, которую ожидает встретить загрузчик eBPF (программа не загрузится, если будет найдено другое имя раздела, однако мы можем указать ip , чтобы использовать нестандартное имя раздела). Давайте скомпилируем приведенную выше программу и попробуем.

$ clang -Wall -target bpf -c xdp-drop.c -o xdp-drop.o

Бинарный объект может быть загружен в ядро ​​с помощью различных инструментов пользовательского пространства (часть пакета iproute2), наиболее широко используемых tc или ip. XDP поддерживает интерфейсы veth (виртуальный Ethernet), поэтому мгновенный способ увидеть нашу программу в действии - выгрузить ее в существующий интерфейс контейнера. Мы развернем контейнер nginx и запустим пару завитков до и после присоединения программы XDP к интерфейсу. Первая попытка скручивания корневого контекста nginx приводит к успешному получению кода статуса HTTP:

$ curl --write-out '%{http_code}' -s --output /dev/null 172.17.0.4:80 200

Загрузку байт-кода XDP можно выполнить с помощью следующей команды:

$ sudo ip link set dev veth74062a2 xdp obj xdp-drop.o

Мы должны увидеть активированный флаг xdp в интерфейсе veth:

veth74062a2@if16: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500xdp/id:37 qdisc noqueue master docker0 state UP group default link/ether 0a:5e:36:21:9e:63 brd ff:ff:ff:ff:ff:ff link-netnsid 2 inet6 fe80::85e:36ff:fe21:9e63/64 scope link valid_lft forever preferred_lft forever

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

curl: (7) Failed to connect to 172.17.0.4 port 80: No route to host

Когда мы закончим наши эксперименты, программу XDP можно выгрузить с помощью:

$ sudo ip link set dev veth74062a2 xdp off

Программирование XDP в Go

Предыдущий фрагмент кода продемонстрировал некоторые базовые концепции, но для использования сверхспособностей XDP мы собираемся создать немного более сложное программное обеспечение с использованием языка Go - небольшого инструмента, построенного на основе своего рода канонического варианта использования: отбрасывание пакетов для определенных черных списков IP-адреса. Полный исходный код вместе с инструкциями по сборке инструмента доступен в репозитории прямо здесь. Как и в предыдущем сообщении в блоге, мы используем пакет gobpf, который обеспечивает основы для взаимодействия с виртуальной машиной eBPF (загрузка программ в ядро, доступ / управление картами eBPF и многое другое). Многие типы программ eBPF могут быть написаны непосредственно на C и скомпилированы в объектные файлы ELF. К сожалению, программы на основе XDP ELF пока не рассматриваются. В качестве альтернативы, присоединение программ XDP по-прежнему возможно через модули BCC за счет работы с зависимостями libbcc.

Тем не менее, в картах BCC есть важное ограничение, которое не позволяет закрепить их на bpffs (на самом деле, вы можете закрепить карты из пользовательского пространства, но во время начальной загрузки модуля BCC он успешно игнорирует любые закрепленные объекты). Наш инструмент должен проверять карту черного списка, но также иметь возможность добавлять / удалять элементы из нее после того, как программа XDP прикреплена к сетевому интерфейсу и завершится основной процесс.

Этого было достаточно, чтобы рассмотреть возможность поддержки программ XDP в объектах ELF, поэтому мы отправили запрос на вытягивание в надежде включить его в репозиторий восходящего потока. Мы считаем, что это ценное дополнение, способствующее переносимости программ XDP, подобно тому, как пробы ядра могут быть распределены по машинам, даже если они не поставляются с clang, LLVM и другими зависимостями.

Без лишних слов, давайте пробежимся по наиболее важным фрагментам, начиная с кода XDP:

SEC("xdp/xdp_ip_filter") 
int xdp_ip_filter(struct xdp_md *ctx) {     
   void *end = (void *)(long)ctx->data_end;     
   void *data = (void *)(long)ctx->data;     
   u32 ip_src;     
   u64 offset;     
   u16 eth_type;      
   struct ethhdr *eth = data;     
   offset = sizeof(*eth);      
   if (data + offset > end) {     
       return XDP_ABORTED;     
   }     
   eth_type = eth->h_proto;      
   
   /* handle VLAN tagged packet */        
  if (eth_type == htons(ETH_P_8021Q) || eth_type == htons(ETH_P_8021AD)) {              
     struct vlan_hdr *vlan_hdr;            
     vlan_hdr = (void *)eth + offset;           
     offset += sizeof(*vlan_hdr);           
     if ((void *)eth + offset > end)                
        return false;           
     eth_type = vlan_hdr->h_vlan_encapsulated_proto;     
  }      
  /* let's only handle IPv4 addresses */     
  if (eth_type == ntohs(ETH_P_IPV6)) {         
      return XDP_PASS;     
  }      
  struct iphdr *iph = data + offset;     
  offset += sizeof(struct iphdr);     
  /* make sure the bytes you want to read are within the packet's   range before reading them */     
  if (iph + 1 > end) {         
    return XDP_ABORTED;     
  }     
  ip_src = iph->saddr;      
  if (bpf_map_lookup_elem(&blacklist, &ip_src)) {         
      return XDP_DROP;     
  }      
  return XDP_PASS; 
}

Это может показаться немного устрашающим, но, например, давайте проигнорируем блок кода, отвечающий за обработку пакетов с тегами VLAN. Мы начинаем с доступа к пакетным данным из контекста метаданных XDP и приводим указатель к структуре ядра ethddr. Вы также можете заметить несколько условий, которые проверяют границы байтов в пакете. Если вы их опустите, верификатор откажется загружать байт-код XDP. Это обеспечивает соблюдение правил, которые гарантируют запуск программ XDP, не вызывая хаоса в ядре, если код ссылается на недопустимые указатели или нарушает политики безопасности. Оставшаяся часть кода извлекает исходный IP-адрес из IP-заголовка и проверяет его присутствие в карте черного списка. Если поиск успешен, пакет отбрасывается.

Структура Hook отвечает за подключение / отключение программ XDP в сетевом стеке. Он создает и загружает модуль XDP из объектного файла и вызывает методы AttachXDP или RemoveXDP.

Черный список IP-адресов ведется через стандартные карты eBPF. Мы вызываем UpdateElement и DeleteElement для регистрации или удаления записей соответственно. Диспетчер черных списков также содержит метод для отображения доступных IP-адресов на карте.

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

Выводы

XDP постепенно становится стандартом быстрой обработки пакетов в ядре Linux. В этом сообщении в блоге я объяснил основные строительные блоки, из которых состоит экосистема обработки пакетов. Хотя сетевой стек - сложный предмет, создание программ XDP относительно безболезненно из-за программируемой природы eBPF / XDP.

Первоначально опубликовано на https://sematext.com 3 июня 2019 г.