快速上手 超XDP入门
服务器 网络 编程
Lastmod: 2024-09-17
Published: 2024-07-24

概要

这是一个快速推动你体验XDP的文章。
因此,eBPF等的详细说明将被省略。

XDP是一个框架,用于在Linux内核网络栈的初始阶段处理数据包,并允许使用eBPF直接将程序插入网络接口卡(NIC)。

通过在Linux内核的网络栈的初始阶段操作数据包,它可以比iptables等过滤器更快地处理数据。

简单来说,你可以理解为可以将XDP程序通过eBPF附加到NIC接口上。

环境构建

本文以Ubuntu 22.04为基础。
为了控制XDP的NIC通信,建议在虚拟机等环境中进行测试。

  • 安装必要的软件包

    $ sudo apt install build-essential clang llvm gcc-multilib libbpf-dev
    

构建eBPF(XDP)使用clang而非gcc,并且需要安装libbpf-dev以获取ebpf用的头文件。

gcc-multilib稍后在使用asm/types.h时是必要的。

现在就来试试 彻底丢弃所有数据包

确认接口

  • 首先查看接口上是否有通信发生

    $ sudo tcpdump -i eth0 -n
    

通常,如果连接到交换机等,应该会看到ARP等信息。
如果没有任何通信发生,请从外部ping或其他方式发送数据包以确认。

准备XDP程序

  • 使用XDP丢弃所有通信

    • xdp.c
    #include <linux/bpf.h>
    #include <bpf/bpf_helpers.h>
    
    SEC("xdp")
    int xdp_drop(struct xdp_md *ctx) {
        return XDP_DROP;
    }
    
    char _license[] SEC("license") = "MIT";
    
  • #include <linux/bpf.h>

    • 提供Linux的BPF(伯克利数据包过滤器)相关功能的头文件
  • #include <bpf/bpf_helpers.h>

    • 提供编写eBPF程序所需的辅助函数的头文件
  • SEC("xdp")

    • 宏定义指定接下来定义的函数类型为eBPF程序
    • 指定"xdp"意味着接下来定义的函数为XDP程序
  • int xdp_drop(struct xdp_md *ctx)

    • 每次接收到数据包时调用xdp_drop。
      在此程序中,我们使用return XDP_DROP;丢弃所有数据包。
  • char _license[] SEC("license") = "MIT";

    • eBPF程序需要指定许可证才能执行
    • 如果执行由GPL派生的函数,则必须使用GPL,否则将报错

构建

  • 执行构建

    $ clang -O2 -target bpf -c xdp.c -o xdp.o
    
  • 通过将target设置为bpf来构建,将其作为eBPF用对象来构建。

附加到接口

  • 附加到接口

    $ sudo ip link set dev eth0 xdp obj xdp.o sec xdp
    

确认数据包

$ sudo tcpdump -i eth0 -n

应该没有ARP、ICMP等数据包到达。
XDP的处理在tcpdump之前,因此无法通过tcpdump观察到数据包。

分离XDP

$ sudo ip link set dev eth0 xdp off

至此,丢弃所有数据包的XDP程序被分离,
通信应该能够恢复。

更深入地查看(丢弃ICMP)

仅仅丢弃所有数据包对于ip link set down dev eth0来说实在没有太大区别,
因此我们尝试使用XDP仅丢弃ICMP数据包。

  • xdp.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数据包
        return XDP_DROP;
    }
    
    char _license[] SEC("license") = "MIT";
    

我们可以通过ctx提取数据包的指针等信息来进行处理。

  • data
    • 指向数据包数据的指针
  • data_end
    • data的结束指针
struct ethhdr *eth = data;
if ((void *)eth + sizeof(*eth) > data_end)
    return XDP_PASS;

将data指针赋值给以太网头,以便获取以太网头指针。
如果eth的指针加上以太网头大小超过data_end,说明
数据包大小小于以太网头大小,这时将不会处理并将其传递给内核。

if (eth->h_proto != htons(ETH_P_IP))
    return XDP_PASS;

检查以太网头,确保h_proto为ETH_P_IP(IPv4协议 0x0800)。htons是将主机字节序(主机的字节顺序)转换为通信数据包的字节序(网络字节顺序)的宏。htons(主机到网络短整数)

如果以太网包中包含的协议不是IPv4,则将其XDP_PASS并传递给内核。

struct iphdr *ip = data + sizeof(*eth);
if ((void *)ip + sizeof(*ip) > data_end)
    return XDP_PASS;

获取IP头指针。 将data(初始指针)加上以太网头的大小得到以太网头的结束位置,此时ip头指针应为IP头的起始指针。

同时,在这里检查ip头指针的大小是否超过data_end,如果超过则表示不是ip头,返回XDP_PASS并将其传递给内核。

if (ip->protocol != IPPROTO_ICMP)
    return XDP_PASS;

检查ip数据包中包含的协议是否为IPPROTO_ICMP(1)。
如果是其他协议,则XDP_PASS并传递给内核。

struct icmphdr *icmp = (void *)ip + sizeof(*ip);
if ((void *)icmp + sizeof(*icmp) > data_end)
    return XDP_PASS;

获取ICMP头指针。
从ip头起始指针进一项加上ip头的大小。
在这里也再检查一遍是否超过data_end。

到这里为止,未被XDP_PASS的包就是ICMP包,因此可以使用XDP_DROP丢弃数据包达到目的。

  • 注意:在IPPROTO_ICMP时直接丢弃也是合理的选择。

最后

虽然必须用C语言编写可能会感觉稍微复杂,但
实际操作下来发现其实非常简单就可以实现。

下一步 (2024/09/17 更新)