Отладка cети с помощью eBPF

Перевод сделан автором сайта goxpert.ru

Краткое содежание перевода

  • Сети могут вызывать проблемы, и устранение неполадок требует использования инструментов, таких как сетевые пространства имен, виртуальные машины, tc и netfilter.
  • eBPF - это новая технология, которая добавляет улучшения в BPF и позволяет записывать данные в память и редактировать пакеты.
  • eBPF используется для устранения сложных сетевых неполадок и может быть подключена через XDP или tc.
  • Пример использования eBPF включает изменение флагов TCP и обработку пакетов, покидающих виртуальную машину.
  • eBPF становится все более популярным и внедряется непосредственно в сетевые адаптеры оборудования.
  • Хотя eBPF не является панацеей, она является мощным инструментом для сетевой отладки и заслуживает внимания.

Введение (Introduction)

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

К счастью, есть несколько инструментов, которые приходят на помощь: сетевые пространства имен( network namespaces ), виртуальные машины, tcи netfilter. Простые сетевые настройки могут быть воспроизведены с помощью сетевых пространств имен и veth устройств, в то время как более сложные настройки требуют подключения виртуальных машин к программному мосту и использования стандартных сетевых инструментов, таких как iptables или tc, для имитации некорректного поведения. Если у вас возникла проблема с ответами ICMP, генерируемыми из-за сбоя SSH-сервера, iptables -A INPUT -p tcp –dport 22 -j REJECT –reject-with icmp-host-unreachable использование правильного пространства имен или виртуальной машины может помочь.

В этой статье описывается использование eBPF ( extended BPF ), расширенной версии Berkeley Packet Filter, для устранения сложных сетевых неполадок. eBPF - довольно новая технология, и проект все еще находится на ранней стадии, документация и SDK еще не готовы. Но это должно улучшиться, особенно с учетом того, что XDP (экспресс-путь передачи данных - eXpress Data Path) поставляется в Red Hat Enterprise Linux 8, который вы можете загрузить и запустить прямо сейчас.

Хотя eBPF и не является серебряной пулей, я думаю, что это очень мощный инструмент для сетевой отладки, и он заслуживает внимания. Я уверен, что он сыграет действительно важную роль в будущем сетей.

Проблема (The problem)

Я отлаживал проблему с сетью Open vSwitch (OVS), повлиявшую на очень сложную установку: некоторые TCP-пакеты были скремблированы(разрознены) и доставлялись не по порядку, а пропускная способность между виртуальными машинами падала с устойчивых 6 Гбит / с до колеблющихся 2-4 Гбит / с. После некоторого анализа выяснилось, что первый TCP-пакет каждого соединения с установленным флагом PSH отправлялся не по порядку: только первый и только по одному на каждое соединение.

Я попытался повторить настройку с двумя виртуальными машинами, и после множества справочных страниц и поисков в Интернете я обнаружил, что оба iptables и nftables не могут изменять флаги TCP, хотя tc могли, но это может только перезаписать флаги, нарушая новые подключения и TCP в целом.

Вероятно, я мог бы справиться с этим, используя комбинацию iptables mark, conntrack и tc, но потом я подумал: это могло бы быть работой для eBPF.

Что такое eBPF? (What is eBPF?)

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

eBPF часто называют BPF, в то время как BPF называется cBPF (классический BPF), поэтому слово BPF может использоваться для обозначения обоих, в зависимости от контекста: здесь я всегда имею в виду расширенную версию.

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

  • Циклы запрещены, поэтому программа завершит работу через определенное время.
  • Он не может получить доступ к памяти, кроме стека и резервного буфера.
  • Могут вызываться только функции ядра из белого списка.

Загруженная программа может быть загружена в ядро многими способами, выполняя множество операций отладки и трассировки. В данном случае нас интересует, как eBPF работает с сетевой подсистемой. Есть два способа использования программы eBPF:

  • Подключается через XDP к самому раннему пути RX физической или виртуальной сетевой карты
  • Подключается через tc к qdisc точно так же, как обычное действие, при входе или выходе

Чтобы создать программу eBPF для подключения, достаточно написать некоторый код на C и преобразовать его в байт-код. Ниже простой пример использования XDP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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 */
/* верификатору eBPF требуется проверка работоспособности */
if (icmph + 1 > data_end)
return XDP_PASS;

/* matched a pong packet */
/ * соответствует пакету pong */
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 */
/* сохраните старый TTL для пересчета контрольной суммы */
uint16_t *ttlproto = (uint16_t *)&iph->ttl;
uint16_t old_ttlproto = *ttlproto;

/* set the TTL to a pseudorandom number 1 < x < TTL */
/* установите для TTL псевдослучайное число 1 < x < TTL */
iph->ttl = bpf_get_prandom_u32() % iph->ttl + 1;

/* recalculate the checksum; otherwise, the IP stack will drop it */
/* пересчитайте контрольную сумму; в противном случае стек IP-адресов удалит ее */
csum_replace2(&iph->check, old_ttlproto, *ttlproto);
}

return XDP_PASS;
}

char _license[] SEC("license") = "GPL";

Приведенный выше фрагмент, лишенный include инструкций, помощников и всего ненужного кода, представляет собой программу XDP, которая изменяет TTL полученных эхо-ответов ICMP, а именно pongs, на случайное число. Основная функция получает struct xdp_md, который содержит два указателя на начало и конец пакета.

Чтобы скомпилировать наш код в байт-код eBPF, необходим компилятор с его поддержкой. Clang поддерживает его и создает байт-код eBPF, указывая bpf в качестве целевого во время компиляции:

1
$ clang -O2 -target bpf -c xdp_manglepong.c -o xdp_manglepong.o

Приведенная выше команда создает файл, который выглядит как обычный объектный файл, но при проверке вы увидите, что указанный тип компьютера будет Linux eBPF, а не собственный тип операционной системы:

1
2
3
4
5
6
7
8
9
10
11
$ 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 пакета, используя следующий синтаксис:

1
# ip -force link set dev wlan0 xdp object xdp_manglepong.o verbose

Эта команда определяет целевой интерфейс wlan0, и с помощью -force опции она перезапишет любой существующий, уже загруженный код eBPF. После загрузки байт-кода eBPF система ведет себя следующим образом:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ ping -c10 192.168.85.1
PING 192.168.85.1 (192.168.85.1) 56(84) байт данных.
64 байта из 192.168.85.1: icmp_seq = 1 ttl = 41 time = 0,929 мс
64 байта из 192.168.85.1: icmp_seq = 2 ttl = 7 time = 0,954 мс
64 байта из 192.168.85.1: icmp_seq = 3 ttl = 17 time = 0.944 мс
64 байта из 192.168.85.1: icmp_seq = 4 ttl = 64 time = 0.948 мс
64 байта из 192.168.85.1: icmp_seq = 5 ttl = 9 time = 0.803 мс
64 байта из 192.168.85.1: icmp_seq = 6 ttl = 22 time = 0,780 мс
64 байта из 192.168.85.1: icmp_seq = 7 ttl = 32 time = 0.847 мс
64 байта из 192.168.85.1: icmp_seq = 8 ttl = 50 time = 0,750 мс
64 байта из 192.168.85.1: icmp_seq = 9 ttl = 24 time = 0,744 мс
64 байта из 192.168.85.1: icmp_seq = 10 ttl = 42 time = 0.791 мс

--- статистика пинга 192.168.85.1 ---
10 переданных пакетов, 10 принятых, потеря пакетов 0%, время 125 мс
rtt min / avg / max / mdev = 0.744/0.849/0.954/0.082 мс

Каждый полученный пакет проходит через eBPF, который в конечном итоге выполняет некоторое преобразование и решает отбросить или пропустить пакет.

Как eBPF может помочь (How eBPF can help)

Возвращаясь к исходной сетевой проблеме, мне нужно было изменить некоторые флаги TCP, только по одному на соединение, и ни iptables ни tc не разрешать это делать. Написать код на C для этого сценария было бы очень просто: настройте две виртуальные машины, соединенные мостом OVS, и просто подключите eBPF к одному из двух виртуальных устройств виртуальной машины.

Это выглядит как хорошее решение, но вы должны учитывать, что XDP поддерживает только обработку принятых пакетов, и подключение eBPF по rx пути принимающей виртуальной машины никак не повлияет на коммутатор.

Чтобы должным образом решить эту проблему, eBPF должен быть загружен с помощью tc и подключен по выходному пути внутри виртуальной машины, поскольку tc программы eBPF могут загружаться и подключаться к qdisc точно так же, как любое другое действие. Для обработки пакетов, покидающих хост, необходим выходной qdisc, к которому нужно подключить eBPF.

При загрузке программы eBPF между XDP и tc API есть небольшие различия: имя раздела по умолчанию отличается, аргумент функции main имеет другой структурный тип, и возвращаемые значения отличаются, но это не является большой проблемой. Ниже приведен фрагмент программы, которая выполняет искажение TCP при подключении к tc действию:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#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 */
/* верификатору eBPF требуется проверка работоспособности */
if ((void *)(tcphdr + 1) > data_end)
return TC_ACT_OK;

/* skip non-TCP packets */
/* пропускает пакеты, отличные от TCP */
if (eth->h_proto != __constant_htons(ETH_P_IP) || iph->protocol != IPPROTO_TCP)
return TC_ACT_OK;

/* incompatible flags, or PSH already set */
/* флаги несовместимости или PSH уже установлены */
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, следующим образом:

1
clang -O2 -целевой bpf -c tcp_psh.c -o tcp_psh.o

Но загрузка отличается:

1
2
# tc qdisc add dev eth0 clsact
# tc filter add dev eth0 egress matchall action bpf object-file tcp_psh.o

На этом этапе eBPF загружен в нужное место, а пакеты, покидающие виртуальную машину, искажены. Проверив полученные пакеты от второй виртуальной машины, вы можете увидеть следующее:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# tcpdump -tnni eth0 -Q in
[1579537.890082] device eth0 entered promiscuous mode
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
IP 192.168.123.1.39252 > 192.168.123.2.5201: Flags [.], seq 809667041:809681521, ack 3046223642, length 14480
IP 192.168.123.1.39252 > 192.168.123.2.5201: Flags [.], seq 14480:43440, ack 1, length 28960
IP 192.168.123.1.39252 > 192.168.123.2.5201: Flags [.], seq 43440:101360, ack 1, length 57920
IP 192.168.123.1.39252 > 192.168.123.2.5201: Flags [P.], seq 101360:131072, ack 1, length 29712
IP 192.168.123.1.39252 > 192.168.123.2.5201: Flags [.], seq 131072:145552, ack 1, length 14480
IP 192.168.123.1.39252 > 192.168.123.2.5201: Flags [.], seq 145552:174512, ack 1, length 28960
IP 192.168.123.1.39252 > 192.168.123.2.5201: Flags [.], seq 174512:210712, ack 1, length 36200
IP 192.168.123.1.39252 > 192.168.123.2.5201: Flags [.], seq 210712:232432, ack 1, length 21720
IP 192.168.123.1.39252 > 192.168.123.2.5201: Flags [.], seq 232432:246912, ack 1, length 14480
IP 192.168.123.1.39252 > 192.168.123.2.5201: Flags [P.], seq 246912:262144, ack 1, length 15232
IP 192.168.123.1.39252 > 192.168.123.2.5201: Flags [.], seq 262144:276624, ack 1, length 14480
IP 192.168.123.1.39252 > 192.168.123.2.5201: Flags [.], seq 276624:305584, ack 1, length 28960
IP 192.168.123.1.39252 > 192.168.123.2.5201: Flags [.], seq 305584:363504, ack 1, length 57920
IP 192.168.123.1.39252 > 192.168.123.2.5201: Flags [P.], seq 363504:393216, ack 1, length 29712
IP 192.168.123.1.39252 > 192.168.123.2.5201: Flags [.], seq 393216:407696, ack 1, length 14480
IP 192.168.123.1.39252 > 192.168.123.2.5201: Flags [.], seq 407696:436656, ack 1, length 28960
IP 192.168.123.1.39252 > 192.168.123.2.5201: Flags [.], seq 436656:494576, ack 1, length 57920
IP 192.168.123.1.39252 > 192.168.123.2.5201: Flags [P.], seq 494576:524288, ack 1, length 29712
IP 192.168.123.1.39252 > 192.168.123.2.5201: Flags [.], seq 524288:538768, ack 1, length 14480
IP 192.168.123.1.39252 > 192.168.123.2.5201: Flags [.], seq 538768:567728, ack 1, length 28960
IP 192.168.123.1.39252 > 192.168.123.2.5201: Flags [.], seq 567728:625648, ack 1, length 57920
IP 192.168.123.1.39252 > 192.168.123.2.5201: Flags [.], seq 625648:627096, ack 1, length 1448
IP 192.168.123.1.39252 > 192.168.123.2.5201: Flags [P.], seq 627096:655360, ack 1, length 28264

tcpdump подтверждает, что новый код eBPF работает, и примерно в 1 из каждых 10 TCP-пакетов установлен флаг PSH. Используя всего 20 строк кода на C, мы выборочно искажали TCP-пакеты, покидающие виртуальную машину, воспроизводя ошибку, возникшую в полевых условиях, и все это без перекомпиляции какого-либо драйвера и даже без перезагрузки! Это значительно упростило проверку исправления Open vSwitch способом, который невозможно было выполнить с помощью других инструментов.

Заключение

eBPF - довольно новая технология, и сообщество придерживается твердого мнения по поводу ее внедрения. Также стоит отметить, что проекты на основе eBPF, такие как bpfilter, становятся все более популярными, и, как следствие, различные поставщики оборудования начинают внедрять поддержку eBPF непосредственно в свои сетевые карты(NIC).

Хотя eBPF не является серебряной пулей, и им не следует злоупотреблять, я думаю, что это очень мощный инструмент для сетевой отладки, и он заслуживает внимания. Я уверен, что он сыграет действительно важную роль в будущем сетей.

Дополнительные ресурсы

Статьи об Open vSwitch
Статьи об открытой виртуальной сети
Представляем stapbpf - новый серверный модуль BPF от SystemTap

Последнее обновление: 14 января 2022 г.

Вот вам и eBPF

Поделиться