Быстрый старт: Введение в XDP
сервер сеть программирование
Lastmod: 2024-09-17
Published: 2024-07-24

Обзор

Это статья о быстром знакомстве с XDP.
Поэтому я опущу подробные объяснения, такие как eBPF.

XDP — это фреймворк для обработки пакетов на самом раннем этапе сетевого стека ядра Linux, который позволяет непосредственно вставлять программы в NIC (сетевую карту) с помощью eBPF.

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

В целом, можно считать, что программу XDP можно прикрепить к интерфейсу NIC, используя eBPF.

Настройка окружения

Мы предполагаем использование Ubuntu 22.04.
Рекомендуется протестировать управление трафиком NIC с помощью виртуальных машин, таких как VM, с помощью XDP.

  • Установка необходимых пакетов

    $ sudo apt install build-essential clang llvm gcc-multilib libbpf-dev
    

Для сборки eBPF (XDP) используется clang, а не gcc, и потребуется установить libbpf-dev для заголовочных файлов eBPF.

gcc-multilib будет нужен позже для использования asm/types.h.

Давайте начнем. Пропустим все пакеты

Проверка интерфейса

  • Сначала проверим, прибывает ли трафик на интерфейс

    $ sudo tcpdump -i eht0 -n
    

Обычно, если он подключен к коммутатору и т. д., должно приходить что-то, например, ARP.
Если трафика нет, попытайтесь отправить ping снаружи, чтобы проверить пакеты.

Подготовка программы XDP

  • Программа XDP для сброса всего трафика

    • xdp.c
    #include <linux/bpf.h>
    #include <bpf/bpf_helpers.h>
    
    SEC("xdp")
    int xdp_drop(struct xdp_md *ctx) {
        return XDP_DROP;
    }
    
    char _license[] SEC("license") = "MIT";
    
  • #include <linux/bpf.h>

    • Заголовочный файл, предоставляющий функции, связанные с BPF (Berkeley Packet Filter) для Linux.
  • #include <bpf/bpf_helpers.h>

    • Заголовочный файл, предоставляющий вспомогательные функции, необходимые для написания программ eBPF.
  • SEC("xdp")

    • Макрос, указывающий, какого типа eBPF программа будет определена ниже.
    • Указав “xdp”, мы указываем, что определяемая функция будет программой XDP.
  • int xdp_drop(struct xdp_md *ctx)

    • Функция xdp_drop вызывается каждый раз при получении пакета.
      В этом случае программа возвращает return XDP_DROP;, чтобы сбросить все пакеты.
  • char _license[] SEC("license") = "MIT";

    • Для выполнения программы eBPF требуется указать лицензию.
    • Если вы выполняете функции, основанные на GPL, но не указали GPL, возникнет ошибка.

Сборка

  • Соберите программу

    $ clang -O2 -target bpf -c xdp.c -o xdp.o
    
  • Указав target как bpf, программа будет собрана в объект для eBPF.

Присоединение к интерфейсу

  • Присоединение к интерфейсу

    $ sudo ip link set dev eth0 xdp obj xdp.o sec xdp
    

Проверка пакетов

$ sudo tcpdump -i eth0 -n

Теперь пакеты ARP, ICMP и т. д. больше не должны поступать.
XDP обрабатывается прежде, чем tcpdump, поэтому вы не сможете наблюдать пакеты с помощью tcpdump.

Отключение XDP

$ sudo ip link set dev eth0 xdp off

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

Давайте посмотрим глубже (Сброс ICMP)

Отключение всех пакетов было бы эквивалентом ip link set down dev eth0, и это не очень полезно,
поэтому давайте попробуем сбросить только ICMP, используя XDP.

  • xdp.c
    #include <linux/bpf.h>
    #include <bpf/bpf_helpers.h>
    #include <linux/if_ether.h>
    #include <linux/ip.h>
    #include <linux/icmp.h>
    #include <arpa/inet.h>
    
    SEC("xdp")
    int xdp_drop_icmp(struct xdp_md *ctx) {
        void *data_end = (void *)(unsigned long)ctx->data_end;
        void *data = (void *)(unsigned long)ctx->data;
    
        // Получаем указатель на Ethernet заголовок
        struct ethhdr *eth = data;
        if ((void *)eth + sizeof(*eth) > data_end)
            return XDP_PASS;
    
        // Проверяем, содержит ли Ethernet-кадр IP-пакет
        if (eth->h_proto != htons(ETH_P_IP))
            return XDP_PASS;
    
        // Получаем указатель на IP заголовок
        struct iphdr *ip = data + sizeof(*eth);
        if ((void *)ip + sizeof(*ip) > data_end)
            return XDP_PASS;
    
        // Проверяем, является ли протокол ICMP
        if (ip->protocol != IPPROTO_ICMP)
            return XDP_PASS;
    
        // Получаем указатель на ICMP заголовок
        struct icmphdr *icmp = (void *)ip + sizeof(*ip);
        if ((void *)icmp + sizeof(*icmp) > data_end)
            return XDP_PASS;
    
        // Сбрасываем ICMP пакеты
        return XDP_DROP;
    }
    
    char _license[] SEC("license") = "MIT";
    

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

  • data
    • Указатель на данные пакета
  • data_end
    • Указатель на конец data
struct ethhdr *eth = data;
if ((void *)eth + sizeof(*eth) > data_end)
    return XDP_PASS;

Здесь мы присваиваем указатель eth указателю data.
Если указатель eth + размер заголовка eth больше data_end, это означает, что
размер пакета меньше размера Ethernet заголовка, поэтому мы не обрабатываем и передаем в ядро.

if (eth->h_proto != htons(ETH_P_IP))
    return XDP_PASS;

Здесь мы проверяем заголовок eth и убеждаемся, что h_proto равен ETH_P_IP (IPv4 протокол 0x0800). htons — это макрос, который преобразует порядок байтов хоста в порядок байтов сети.

Если протокол, содержащийся в Ethernet-кадре, не IPv4, мы возвращаем XDP_PASS и передаем в ядро.

struct iphdr *ip = data + sizeof(*eth);
if ((void *)ip + sizeof(*ip) > data_end)
    return XDP_PASS;

Здесь мы получаем указатель на заголовок IP.
Мы добавляем размер Ethernet заголовка к data (исходный указатель), чтобы получить указатель на начало заголовка IP.

Также мы проверяем, что указатель на заголовок IP меньше data_end. Если это не так, мы используем XDP_PASS и передаем в ядро.

if (ip->protocol != IPPROTO_ICMP)
    return XDP_PASS;

Здесь мы проверяем, является ли протокол в IP-пакете равным IPPROTO_ICMP (1).
Если это не ICMP, мы возвращаем XDP_PASS и передаем в ядро.

struct icmphdr *icmp = (void *)ip + sizeof(*ip);
if ((void *)icmp + sizeof(*icmp) > data_end)
    return XDP_PASS;

Мы получаем указатель на заголовок ICMP.
Указатель на заголовок IP увеличивается на размер заголовка IP.
Мы также проверяем, не превышает ли он значение data_end.

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

В заключение

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

Следующий шаг (Добавлено 2024/09/17)