快速入门 超XDP入门 Part3 (ICMP Echo Reply篇 其二)
服务器 网络 编程
Published: 2025-03-06

在上一篇文章中,我们编写了一个程序来返回ICMP Echo Reply。
虽然实现了返回ICMP Echo Reply的程序,但发现ICMP的校验和计算不正确。

这次,我想写一下如何正确计算ICMP的校验和并返回。

不要懒惰,趁着劲儿连写了两天,请表扬我

ICMP的校验和

  • ICMP头结构
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |      Type      |     Code      |          Checksum           |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |           Identifier           |        Sequence Number      |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                             Data                              |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    

ICMP头包含Checksum,由于我们将ICMP Echo Request的Type修改为ICMP Echo Reply,因此需要重新计算Checksum的值。

ICMP的校验和计算是将头和整个数据部分按16位单位相加,如果发生进位,则将该值再加一次。最后取反(取1的补数)以获得最终的校验和。

关于这方面的计算方法在 RFC 1624 中有详细记载。请注意,参考RFC 1141会导致差分计算失败,因为RFC 1141中的差分计算描述是错误的,因此被RFC 1624所修正。

有过没有意识到更新,反而查看RFC 1141,浪费了无尽的时间的经历。

RFC并不是别人翻译的,一定要阅读原文。。。(自我提醒)

每16位相加后,最后取1的补数进行计算,因此实际上只需对修改部分进行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);
    // 取1的补数,返回最终的校验和
    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;

    // 获取以太网头的指针
    struct ethhdr *eth = data;
    if ((void *)eth + sizeof(*eth) > data_end)
        return XDP_PASS;

    // 确认以太网帧包含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类型修改为ICMP Echo Reply
    icmp->type = ICMP_ECHOREPLY;

    // 初始化校验和为0
    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);
    // 取1的补数,返回最终的校验和
    return (__u16)~csum;
}

csum_fold 函数用于将32位的校验和压缩为16位。bpf_csum_diff() 的返回值为32位,可能会产生进位。因此,首先将高16位的值加到低16位,再进行一次处理。最后通过取1的补数来求得最终的校验和。

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地址。

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类型修改为ICMP Echo Reply
    icmp->type = ICMP_ECHOREPLY;

    // 初始化校验和为0
    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_addrswap_ip_addr 的功能被封装为函数。

复制icmp,将icmp->type修改为ICMP_ECHOREPLY
然后计算校验和。

通过icmp->checksum = 0;先初始化校验和为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字段本来就需要重新计算。

然而,由于我们只是按16位单位互换源IP和目标IP,因此从计算方法上,结果的校验和实际上不会改变。
这是因为“当IP地址按16位单位互换时,作为累加值的1的补数校验和不会改变”的特性。因此,这一次正好没有受到影响,但如果更改其他字段,则必须重新计算IP的校验和。

根据需要,应像ICMP头一样重新计算IP头的校验和。

计算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);
    // 取1的补数,返回最终的校验和
    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;

    // 获取以太网头的指针
    struct ethhdr *eth = data;
    if ((void *)eth + sizeof(*eth) > data_end)
        return XDP_PASS;

    // 确认以太网帧包含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类型修改为ICMP Echo Reply
    icmp->type = ICMP_ECHOREPLY;

    // 初始化校验和为0
    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";

在修改swap_ip_addr之前,先复制IP头,并通过bpf_csum_diff计算差分,与ICMP的校验和类似。这样,IP头的校验和也能正确计算。

结论

  • 我们实现了一个XDP程序,通过XDP对ICMP Echo Request返回ICMP Echo Reply。
  • 介绍了如何计算ICMP的校验和并正确返回。
  • 校验和的计算只需对修正部分进行16位单位的差分,不需要重新计算所有数据。
  • 使用bpf_csum_diff()函数可以轻松地进行校验和计算。