快速上手 超级XDP入门 Part3(ICMP Echo Reply篇 其1)
服务器 网络 编程
Lastmod: 2025-03-06
Published: 2025-03-05

在上一篇文章中,我们使用XDP的eBPF Map与用户空间进行了数据交换。

在前一篇文章中,我们写了一个程序,通过XDP程序丢弃所有数据包。

这次我们将编写一个程序来回复ICMP Echo Request的ICMP Echo Reply。

返回ICMP Echo Reply

为了回复ICMP Echo Request的ICMP Echo Reply,需要以下步骤。

  1. 接收ICMP Echo Request的数据包
  2. 将ICMP Echo Reply的标志更改
  3. 交换源和目标的IP地址
  4. 交换以太网头的源和目标MAC地址
  5. 发送数据包

只丢弃ICMP Echo Request

首先,我们希望能够处理ICMP Echo Request,因此编写一个只丢弃ICMP Echo Request的程序。

  • xdp_echo_request_drop.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;
    
        // 获取以太网头指针
        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;
    
        // 丢弃ICMP Echo Request的数据包
        return XDP_DROP;
    }
    
    char _license[] SEC("license") = "MIT";
    

这段代码与之前在手っ取り早くやってみる 超XDP入門中创建的程序几乎相同。

  // 允许非ICMP Echo Request的数据包通过
  if (icmp->type != ICMP_ECHO)
      return XDP_PASS;

在这里,除了ICMP Echo Request以外的数据包都会被允许通过。

将ICMP Echo改为Reply并返回

接下来,我们将编写一个程序来回复ICMP Echo Request的ICMP Echo Reply。

  1. 将ICMP Echo Reply的标志更改
// 将ICMP Echo Reply的标志更改
icmp->type = ICMP_ECHOREPLY;

将类型从ICMP_ECHO (8)更改为ICMP_ECHOREPLY (0)。

  1. 交换源和目标的IP地址
// 交换源和目标的IP地址
__u32 tmp = ip->saddr;
ip->saddr = ip->daddr;
ip->daddr = tmp;
  1. 交换以太网头的源和目标MAC地址
// 交换以太网头的源和目标
unsigned char tmp_mac[ETH_ALEN];
__builtin_memcpy(tmp_mac, eth->h_dest, ETH_ALEN);
__builtin_memcpy(eth->h_dest, eth->h_source, ETH_ALEN);
__builtin_memcpy(eth->h_source, tmp_mac, ETH_ALEN);
  1. 通过XDP_TX发送数据包
// 发送数据包
return XDP_TX;

到此为止,ICMP Echo Reply的回复程序编写完成。

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;

    // 将ICMP Echo Reply的标志更改
    icmp->type = ICMP_ECHOREPLY;

    // 交换IP头的源和目标地址
    __u32 tmp = ip->saddr;
    ip->saddr = ip->daddr;
    ip->daddr = tmp;

    // 交换以太网头的源和目标
    unsigned char tmp_mac[ETH_ALEN];
    __builtin_memcpy(tmp_mac, eth->h_dest, ETH_ALEN);
    __builtin_memcpy(eth->h_dest, eth->h_source, ETH_ALEN);
    __builtin_memcpy(eth->h_source, tmp_mac, ETH_ALEN);

    // 丢弃ICMP Echo Request的数据包
    // return XDP_DROP;
    return XDP_TX;
}

将程序加载到NIC中

将程序编译并加载到NIC中。

$ clang -O2 -target bpf -c xdp_echo_reply.c -o xdp_echo_reply.o
$ sudo ip link set dev eth0 xdp obj xdp_echo_reply.o

验证运行结果

  • 从客户端发送ping

    $ ping 192.168.XXX.XXX
    
  • 在服务器上运行tcpdump,确认未接收到ICMP Request。 因为XDP直接处理数据包,所以tcpdump无法确认,而Linux内核未回复Echo Reply。

    $ sudo tcpdump -i eth0 icmp
    
  • 从客户端发送ping,确认可以收到ICMP Echo Reply。

    $ sudo tcpdump -i eth0 icmp -vvv
    XX:XX:XX.160507 IP (tos 0x0, ttl 64, id 53521, offset 0, flags [DF], proto ICMP (1), length 84)
        192.168.XXX.1 > 192.168.XXX.2: ICMP echo request, id 58371, seq 684, length 64
    XX:XX:XX.161394 IP (tos 0x0, ttl 64, id 53521, offset 0, flags [DF], proto ICMP (1), length 84)
        192.168.XXX.2 > 192.168.XXX.1: ICMP echo reply, id 58371, seq 684, length 64 (wrong icmp cksum 767b (->7e7b)!)
    

确认ICMP Echo Reply已返回。
然而,显示wrong icmp cksum,这表明ICMP头的校验和计算不正确。

要计算ICMP的校验和并正确回复,应如何处理?如果写作的动力不减,我会继续撰写相关文章。

总结

  • 我们使用XDP实现了一个针对ICMP Echo Request返回ICMP Echo Reply的XDP程序。
  • 步骤包括检测ICMP Echo Request,生成回复数据包并交换MAC地址和IP地址,最后通过XDP_TX直接回复而不经过内核。
  • 最终出现了wrong icmp cksum的错误,确认了需要重新计算ICMP校验和。

下一步 (更新日期: 2025/03/06)

快速入门指南:超级 XDP 入门 第三部分 (ICMP Echo Reply - 第二部分)