概要
这是一个快速推动你体验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;
丢弃所有数据包。
- 每次接收到数据包时调用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语言编写可能会感觉稍微复杂,但
实际操作下来发现其实非常简单就可以实现。