Отладка cети с помощью eBPF (RHEL 8 Beta)
Всех с прошедшими праздниками!
Нашу первую статью после праздников мы решили посвятить линуксу, то есть под наш замечательный курс «Администратор Linux», который у нас входит в когорту самых динамичных курсов, то есть с наиболее актуальным материалами и практиками. Ну и, соответственно, мы предлагаем интересные статьи и открытый урок.
Введение
Работа с сетью — увлекательное занятие, но избежать проблем удается не всегда. Устранение неполадок может быть сложным занятием, также как и попытки воспроизвести неправильное поведение, происходящее «в полевых условиях».
К счастью, существуют инструменты, способные с этим помочь: сетевые пространства имен, виртуальные машины, tc
и netfilter
. Простые сетевые настройки могут быть воспроизведены с помощью сетевых пространств имен и veth-устройств, в то время как более сложные настройки требуют соединения виртуальных машин программным мостом и использования стандартных сетевых инструментов, например, iptables
или tc
, для симуляции некорректного поведения. При наличии проблемы с ICMP-ответами, сгенерированными при падении SSH-сервера, iptables -A INPUT -p tcp --dport 22 -j REJECT --reject-with icmp-host-unreachable
в правильном пространстве имен может помочь решить проблему.
В этой статье описывается устранение сложных проблем сети с помощью eBPF (extended BPF), расширенной версии Пакетного Фильтра Беркли. eBPF — сравнительно новая технология, проект находится на ранней стадии, поэтому документация и SDK пока не готовы. Но будем надеяться на улучшения, особенно с учетом того, что XDP (eXpress Data Path) поставляется с Red Hat Enterprise Linux 8 Beta, которую можно загрузить и запустить уже сейчас.
eBPF не решит всех проблем, но все равно это мощный инструмент для сетевой отладки, который заслуживает внимания. Уверен, он будет играть действительно важную роль в будущем сетей.
Проблема
Я занимался отладкой проблемы сети Open vSwitch (OVS), которая затрагивала очень сложную установку: некоторые TCP-пакеты были разрознены и доставлялись в неправильном порядке, а пропускная способность виртуальных машин падала со стабильных 6 Гб/с до колеблющихся 2–4 Гб/с. Анализ показал, что первый TCP-пакет каждого соединения с PSH-флагом отправлялся в неправильном порядке: только первый и только один за соединение.
Я попытался воспроизвести эту настройку с двумя виртуальными машинами и, спустя множество справочных статей и поисковых запросов, обнаружил, что ни iptables
, ни nftables
не могут манипулировать флагами TCP, в то время как tc
может, но только перезаписывая флаги и прерывая новые соединения и TCP в целом.
Возможно, удалось бы решить проблему с помощью комбинации iptables
, conntrack
и tc
, но я решил, что это отличная работа для eBPF.
Что такое eBPF?
eBPF — расширенная версия Пакетного Фильтра Беркли. Она привносит большое количество улучшений в BPF. В частности, позволяет писать в памяти, а не только читать, поэтому пакеты можно не только фильтровать, но и редактировать.
Часто eBPF называют просто BPF, а сам BPF называют cBPF (classic (классический) BPF), поэтому слово «BPF» может использоваться для обозначения обеих версий, в зависимости от контекста: в этой статье я всегда говорю о расширенной версии.
«Под капотом» у eBPF очень простая виртуальная машина, которая может выполнять небольшие фрагменты байт-кода и править некоторые буферы памяти. В eBPF есть ограничения, защищающие его от злонамеренного использования:
- Циклы запрещены, чтобы программа всегда завершалась в определенное время;
- Он может получить доступ к памяти только через стек и scratch-буфер;
- Могут быть вызваны только разрешенные функции ядра.
Программа может быть загружена в ядро разными способами при помощи отладки и трассировки. В нашем случае, интересует работа eBPF с сетевыми подсистемами. Есть два способа использования eBPF программы:
- Подключенной через XDP к началу RX-пути физической или виртуальной сетевой платы;
- Подключенной через
tc
к qdisc во входе или выходе.
Чтобы создать eBPF программу для подключения, достаточно написать код на C и сконвертировать его в байт-код. Ниже представлен простой пример с использованием XDP:
SEC("prog")
int xdp_main(struct xdp_md *ctx)
{
void *data_end = (void *)(uintptr_t)ctx->data_end;
void *data = (void *)(uintptr_t)ctx->data;
struct ethhdr *eth = data;
struct iphdr *iph = (struct iphdr *)(eth + 1);
struct icmphdr *icmph = (struct icmphdr *)(iph + 1);
/* sanity check needed by the eBPF verifier */
if (icmph + 1 > data_end)
return XDP_PASS;
/* matched a pong packet */
if (eth->h_proto != ntohs(ETH_P_IP) ||
iph->protocol != IPPROTO_ICMP ||
icmph->type != ICMP_ECHOREPLY)
return XDP_PASS;
if (iph->ttl) {
/* save the old TTL to recalculate the checksum */
uint16_t *ttlproto = (uint16_t *)&iph->ttl;
uint16_t old_ttlproto = *ttlproto;
/* set the TTL to a pseudorandom number 1 < x < TTL */
iph->ttl = bpf_get_prandom_u32() % iph->ttl + 1;
/* recalculate the checksum; otherwise, the IP stack will drop it */
csum_replace2(&iph->check, old_ttlproto, *ttlproto);
}
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
Фрагмент выше, без выражений include
, хелперов и опционального кода — XDP-программа, которая меняет TTL получаемых эхо-ответов ICMP, а именно pong«ов, на случайное число. Основная функция получает структуру xdp_md
, в которой находится два указателя на начало и конец пакета.
Для компиляции нашего кода в eBPF байт-код, требуется компилятор с соответствующей поддержкой. Clang поддерживает его и создает байт-код eBPF путем уточнения bpf в качестве цели во время компиляции:
$ clang -O2 -target bpf -c xdp_manglepong.c -o xdp_manglepong.o
Команда выше создает файл, который, на первый взгляд, кажется обычным объектным файлом, но при ближайшем рассмотрении, оказывается, что указанным типом компьютера будет Linux eBPF, а не нативный тип операционной системы:
$ readelf -h xdp_manglepong.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Linux BPF <--- HERE
[...]
Получив обертку обычного объектного файла, eBPF программа готова к загрузке и подключению к устройству через XDP. Это можно сделать с помощью ip
из пакета iproute2
со следующим синтаксисом:
# ip -force link set dev wlan0 xdp object xdp_manglepong.o verbose
Эта команда указывает целевой интерфейс wlan0 и, благодаря параметру -force, перезаписывает любой существующий eBPF код, который уже был загружен. После загрузки eBPF байт-кода, система ведет себя следующим образом:
$ ping -c10 192.168.85.1
PING 192.168.85.1 (192.168.85.1) 56(84) bytes of data.
64 bytes from 192.168.85.1: icmp_seq=1 ttl=41 time=0.929 ms
64 bytes from 192.168.85.1: icmp_seq=2 ttl=7 time=0.954 ms
64 bytes from 192.168.85.1: icmp_seq=3 ttl=17 time=0.944 ms
64 bytes from 192.168.85.1: icmp_seq=4 ttl=64 time=0.948 ms
64 bytes from 192.168.85.1: icmp_seq=5 ttl=9 time=0.803 ms
64 bytes from 192.168.85.1: icmp_seq=6 ttl=22 time=0.780 ms
64 bytes from 192.168.85.1: icmp_seq=7 ttl=32 time=0.847 ms
64 bytes from 192.168.85.1: icmp_seq=8 ttl=50 time=0.750 ms
64 bytes from 192.168.85.1: icmp_seq=9 ttl=24 time=0.744 ms
64 bytes from 192.168.85.1: icmp_seq=10 ttl=42 time=0.791 ms
--- 192.168.85.1 ping statistics ---
10 packets transmitted, 10 received, 0% packet loss, time 125ms
rtt min/avg/max/mdev = 0.744/0.849/0.954/0.082 ms
Каждый пакет проходит через eBPF, который в конечном итоге вносит некоторые изменения и решает бросить ли пакет или пропустить.
Как eBPF может помочь
Возвращаясь к изначальной сетевой проблеме, вспомним, что нужно было маркировать несколько TCP флагов, по одному за соединение, и ни iptables
, ни tc
не могли этого сделать. Писать код для этого сценария совсем несложно: настройте две виртуальные машины, соединенные OVS-мостом, и просто подключите eBPF к одному из виртуальных устройств ВМ.
Звучит как отличное решение, но стоит учитывать, что XDP поддерживает только обработку полученных пакетов, и подключение eBPF к пути rx
принимающей виртуальной машины не окажет никакого воздействия на коммутатор.
Чтобы решить эту проблему, eBPF должен быть загружен с помощью tc
и подключен к выходному пути ВМ, потому что tc
может загружать и подключать eBPF программы к qdisk. Чтобы промаркировать пакеты, покидающие хост, eBPF необходимо подключить к выходному qdisk.
При загрузке eBPF программы, между XDP
и tc
API есть некоторые отличия: по умолчанию разные названия разделов, отличается тип структуры аргумента главной функции, разные возвращаемые значения. Но это не проблема. Ниже представлен фрагмент программы, маркирующей TCP при присоединении к tc-действию:
#define RATIO 10
SEC("action")
int bpf_main(struct __sk_buff *skb)
{
void *data = (void *)(uintptr_t)skb->data;
void *data_end = (void *)(uintptr_t)skb->data_end;
struct ethhdr *eth = data;
struct iphdr *iph = (struct iphdr *)(eth + 1);
struct tcphdr *tcphdr = (struct tcphdr *)(iph + 1);
/* sanity check needed by the eBPF verifier */
if ((void *)(tcphdr + 1) > data_end)
return TC_ACT_OK;
/* skip non-TCP packets */
if (eth->h_proto != __constant_htons(ETH_P_IP) || iph->protocol != IPPROTO_TCP)
return TC_ACT_OK;
/* incompatible flags, or PSH already set */
if (tcphdr->syn || tcphdr->fin || tcphdr->rst || tcphdr->psh)
return TC_ACT_OK;
if (bpf_get_prandom_u32() % RATIO == 0)
tcphdr->psh = 1;
return TC_ACT_OK;
}
char _license[] SEC("license") = "GPL";
Компиляция в байт-код произведена, как показано в XDP примере выше, с помощью следующего:
clang -O2 -target bpf -c tcp_psh.c -o tcp_psh.o
Но загрузка отличается:
# tc qdisc add dev eth0 clsact
# tc filter add dev eth0 egress matchall action bpf object-file tcp_psh.o
Теперь eBPF загружен в нужном месте и пакеты, покидающие ВМ, промаркированы. Проверив пакеты, полученные во второй ВМ, увидим следующее:
tcpdump
подтверждает, что новый eBPF-код работает, и примерно 1 из каждых 10 TCP-пакетов имеют установленный флаг PSH. Всего лишь 20 строк C-кода понадобилось для выборочной маркировки TCP-пакетов, покидающих виртуальную машину, воспроизведения ошибки, происходящей «на бою», и все без перекомпиляции или даже перезапуска! Это значительно упростило проверку фикса Open vSwitch, чего невозможно было добиться с помощью других инструментов.
Вывод
eBPF — довольно новая технология, и у сообщество есть четкое мнение по поводу ее внедрения. Также стоит отметить, что проекты на базе eBPF, например bpfilter, становятся все популярней, и, как следствие, многие поставщики оборудования начинают внедрять поддержку eBPF напрямую в сетевые платы.
eBPF не решит всех проблем, поэтому не стоит им злоупотреблять, но все же он остается очень мощным инструментом для сетевой отладки и заслуживает внимания. Уверен, он сыграет важную роль в будущем сетей.
THE END
Ждём ваши комментарии тут, а так же приглашаем посетить наш открытый урок, где если что можно так же позадавать вопросы.