Быстрый способ изучения: Введение в XDP, Часть 3 (ICMP Echo Reply, часть 2)

В предыдущей статье мы написали программу, которая возвращает ICMP Echo Reply с помощью XDP.
Мы реализовали программу, возвращающую ICMP Echo Reply, но оказалось, что контрольная сумма ICMP не была правильно рассчитана.

На этот раз я хотел бы описать, как правильно рассчитать контрольную сумму ICMP и вернуть её.

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

Контрольная сумма ICMP

  • Структура заголовка ICMP
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |      Type      |     Code      |          Checksum           |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |           Identifier           |        Sequence Number      |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                             Data                              |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    

В заголовке ICMP содержится контрольная сумма, и поскольку мы изменили Type с ICMP Echo Request на ICMP Echo Reply, необходимо заново рассчитать значение контрольной суммы.

Расчет контрольной суммы ICMP выполняется путем суммирования заголовка и всех данных по 16 бит, и если происходит перенос, это значение добавляется снова. В конце берется дополнение до единицы (битовая инверсия), чтобы получить окончательную контрольную сумму.

Метод вычисления сюда подробно описан в RFC 1624. Будьте осторожны, так как при ссылке на RFC 1141 могут возникнуть проблемы с расчетом разницы, так как описание разницы в RFC 1141 неверно, и оно было исправлено в RFC 1624.

Я не заметил изменений и посмотрел на RFC 1141, что привело к потере бесконечного времени.

RFC — это не то, что перевели кто-то, поэтому всегда читайте оригинал… (это предостережение себе).

Суммируя по 16 бит и беря дополнение до единицы, на самом деле можно просто получить разницу в исправленных местах по 16 битам, без необходимости пересчитывать все данные.

bpf_csum_diff

При выполнении вычислений контрольной суммы в программе eBPF есть функция bpf_csum_diff. С её помощью можно легко рассчитать контрольную сумму.

bpf_csum_diff — это функция, которая вычисляет изменение контрольной суммы на основе изменений в указанной области памяти по 16 бит. Это позволяет эффективно обновлять контрольные суммы ICMP и IP.

Программа, возвращающая ICMP Echo Reply

#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>

static __always_inline __u16 csum_fold(__u32 csum)
{
    // Суммируем старшие 16 бит с младшими 16 битами
    csum = (csum & 0xffff) + (csum >> 16);
    // Снова добавляем перенос
    csum = (csum & 0xffff) + (csum >> 16);
    // Берем дополнение до единицы и возвращаем окончательную контрольную сумму
    return (__u16)~csum;
}

static __always_inline void swap_eth_addr(__u8 *a, __u8 *b)
{
    __u8 tmp[ETH_ALEN];
    __builtin_memcpy(tmp, a, ETH_ALEN);
    __builtin_memcpy(a, b, ETH_ALEN);
    __builtin_memcpy(b, tmp, ETH_ALEN);
}

static __always_inline void swap_ip_addr(__u32 *a, __u32 *b)
{
    __u32 tmp = *a;
    *a = *b;
    *b = tmp;
}

SEC("xdp")
int xdp_echo_reply(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 Echo Request
    if (icmp->type != ICMP_ECHO)
        return XDP_PASS;

    // Меняем местами MAC-адреса источника и назначения
    swap_eth_addr(eth->h_dest, eth->h_source);
    // Меняем местами IP-адреса источника и назначения
    swap_ip_addr(&ip->saddr, &ip->daddr); 

    // Копируем icmp
    struct icmphdr icmp_before = *icmp;

    // Меняем флаг на ICMP Echo Reply
    icmp->type = ICMP_ECHOREPLY;

    // Инициализируем контрольную сумму нулем
    icmp->checksum = 0;
    __s64 value = bpf_csum_diff((void *)&icmp_before, sizeof(icmp_before), (void *)icmp, sizeof(*icmp), 0);
    if (value >= 0)
        icmp->checksum = csum_fold(value);

    // Пакет ICMP Echo Request будет отброшен
    // return XDP_DROP;
    return XDP_TX;
}

char _license[] SEC("license") = "MIT";
static __always_inline __u16 csum_fold(__u32 csum)
{
    // Суммируем старшие 16 бит с младшими 16 битами
    csum = (csum & 0xffff) + (csum >> 16);
    // Снова добавляем перенос
    csum = (csum & 0xffff) + (csum >> 16);
    // Берем дополнение до единицы и возвращаем окончательную контрольную сумму
    return (__u16)~csum;
}

Функция csum_fold выполняет сжатие 32-битной контрольной суммы до 16 бит. Возвратное значение bpf_csum_diff() является 32-битным и может произойти перенос. Поэтому сначала суммируем старшие 16 бит с младшими 16 битами, затем выполняем еще раз перенос. В конце берется дополнение до единицы (битовая инверсия) для получения окончательной контрольной суммы.

static __always_inline void swap_eth_addr(__u8 *a, __u8 *b)
{
    __u8 tmp[ETH_ALEN];
    __builtin_memcpy(tmp, a, ETH_ALEN);
    __builtin_memcpy(a, b, ETH_ALEN);
    __builtin_memcpy(b, tmp, ETH_ALEN);
}

Функция swap_eth_addr меняет местами MAC-адреса в заголовке Ethernet.

static __always_inline void swap_ip_addr(__u32 *a, __u32 *b)
{
    __u32 tmp = *a;
    *a = *b;
    *b = tmp;
}

Функция swap_ip_addr меняет местами IP-адреса в заголовке IP.

В случае eBPF вызовы функций необходимо инлайнить, поэтому используется __always_inline.

    // Меняем местами MAC-адреса источника и назначения
    swap_eth_addr(eth->h_dest, eth->h_source);
    // Меняем местами IP-адреса источника и назначения
    swap_ip_addr(&ip->saddr, &ip->daddr); 

    // Копируем заголовок ICMP
    struct icmphdr icmp_before = *icmp;

    // Меняем флаг на ICMP Echo Reply
    icmp->type = ICMP_ECHOREPLY;

    // Инициализируем контрольную сумму нулем
    icmp->checksum = 0;
    __s64 value = bpf_csum_diff((void *)&icmp_before, sizeof(icmp_before), (void *)icmp, sizeof(*icmp), 0);
    if (value >= 0)
        icmp->checksum = csum_fold(value);

Эта часть отличается от предыдущей.
Функции swap_eth_addr и swap_ip_addr просто инлайнены, чтобы поменять местами MAC-адреса и IP-адреса.

Мы копируем icmp и изменяем icmp->type на ICMP_ECHOREPLY.
Затем мы рассчитываем контрольную сумму.

icmp->checksum = 0; инициализирует контрольную сумму нулем, после чего мы рассчитываем разность с помощью bpf_csum_diff(). Вернувшееся значение bpf_csum_diff не переносится, поэтому мы используем csum_fold().

Проверка работы

  • Отправка ping с клиента

    $ ping 192.168.XXX.XXX
    
  • После отправки ping с клиента мы проверяем, что получен ICMP Echo Reply.

    $ sudo tcpdump -i eth0 icmp -vvv
    XX:XX:XX.413478 IP (tos 0x0, ttl 64, id 35523, offset 0, flags [DF], proto ICMP (1), length 84)
        192.168.XXX.1 > 192.168.XXX.2: ICMP echo request, id 64346, seq 16, length 64
    XX:XX:XX.414359 IP (tos 0x0, ttl 64, id 35523, offset 0, flags [DF], proto ICMP (1), length 84)
        192.168.XXX.2 > 192.168.XXX.1: ICMP echo reply, id 64346, seq 16, length 64
    

Убедились, что ошибка контрольной суммы устранена, и ICMP Echo Reply возвращается.

На самом деле…

На самом деле, в заголовке IP тоже есть контрольная сумма, и её также необходимо пересчитать.

  • Структура заголовка IP
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | Version |  IHL  |  DSCP  | ECN  |        Total Length         |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |         Identification        |Flags|     Fragment Offset     |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |  Time to Live |   Protocol    |        Header Checksum        |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                      Source IP Address                       |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                   Destination IP Address                     |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                    Options (if any)                          |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    

В поле HeaderChecksum содержится значение, которое необходимо пересчитать.

Тем не менее, в нашем случае, поскольку мы просто меняем местами IP-адрес источника и назначения по 16 бит, способ вычисления контрольной суммы в итоге не изменится.
Это происходит из-за свойства, что “если IP-адреса поменять местами по 16 битам, то контрольная сумма, рассчитанная как дополнение до единицы, не изменится”. Поэтому в данном случае влияние отсутствует, но если бы были изменены другие поля, контрольная сумма IP обязательно должна быть пересчитана.

При необходимости пересчитайте заголовок IP аналогично заголовку ICMP.

Рассчитаем контрольную сумму заголовка IP

#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>

static __always_inline __u16 csum_fold(__u32 csum)
{
    // Суммируем старшие 16 бит с младшими 16 битами
    csum = (csum & 0xffff) + (csum >> 16);
    // Снова добавляем перенос
    csum = (csum & 0xffff) + (csum >> 16);
    // Берем дополнение до единицы и возвращаем окончательную контрольную сумму
    return (__u16)~csum;
}

static __always_inline void swap_eth_addr(__u8 *a, __u8 *b)
{
    __u8 tmp[ETH_ALEN];
    __builtin_memcpy(tmp, a, ETH_ALEN);
    __builtin_memcpy(a, b, ETH_ALEN);
    __builtin_memcpy(b, tmp, ETH_ALEN);
}

static __always_inline void swap_ip_addr(__u32 *a, __u32 *b)
{
    __u32 tmp = *a;
    *a = *b;
    *b = tmp;
}

SEC("xdp")
int xdp_echo_reply(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 Echo Request
    if (icmp->type != ICMP_ECHO)
        return XDP_PASS;

    // Копируем заголовок IP
    struct iphdr ip_before = *ip;

    // Меняем местами MAC-адреса источника и назначения
    swap_eth_addr(eth->h_dest, eth->h_source);
    // Меняем местами IP-адреса источника и назначения
    swap_ip_addr(&ip->saddr, &ip->daddr); 

    // Копируем заголовок ICMP
    struct icmphdr icmp_before = *icmp;

    // Меняем флаг на ICMP Echo Reply
    icmp->type = ICMP_ECHOREPLY;

    // Инициализируем контрольную сумму нулем
    icmp->checksum = 0;
    __s64 value = bpf_csum_diff((void *)&icmp_before, sizeof(icmp_before), (void *)icmp, sizeof(*icmp), 0);
    if (value >= 0)
        icmp->checksum = csum_fold(value);

    // Контрольная сумма заголовка IP
    ip->check = 0;
    value = bpf_csum_diff((void *)&ip_before, sizeof(ip_before), (void *)ip, sizeof(*ip), 0);
    if (value >= 0)
        ip->check = csum_fold(value);

    // Пакет ICMP Echo Request будет отброшен
    // return XDP_DROP;
    return XDP_TX;
}

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

Перед изменением заголовка IP с помощью swap_ip_addr мы копируем его, и аналогично контролю ICMP, мы рассчитываем разницу с помощью bpf_csum_diff. Теперь контрольная сумма заголовка IP будет правильно рассчитана.

Резюме

  • Мы реализовали XDP-программу, которая отвечает на ICMP Echo Request с помощью ICMP Echo Reply.
  • Мы описали, как корректно рассчитывать контрольную сумму ICMP и возвращать её.
  • Расчёт контрольной суммы можно выполнить, используя только разницу в исправленных местах по 16 битам, без необходимости пересчитывать все данные.
  • Используя функцию bpf_csum_diff(), можно легко выполнять вычисления контрольной суммы.