Обзор
Это статья о быстром знакомстве с 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;
, чтобы сбросить все пакеты.
- Функция 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,
на практике это оказывается довольно простым.