概要
在上一篇文章中,我们编写了一个使用XDP程序丢弃所有数据包的程序。
这次我们将编写一个使用eBPF Map来统计丢弃的数据包的程序。
本次依然是一个简单的XDP+eBPF Map的实用话题,因此将省略eBPF等的详细解释和未使用的Map类型的说明。
环境构建
以Ubuntu 22.04为基础。
为了控制NIC的通信,建议在虚拟机等环境中进行测试。
有关设置的详细信息,请参见上一篇文章。
eBPF Map
eBPF Map是用于在内核和用户空间之间共享数据的数据结构。这使得XDP程序可以实时更新数据,而用户空间也可以读取这些数据。
这次我们将编写一个用于统计通过XDP丢弃的数据包数量的程序。
eBPF Map的类型
eBPF Map有多种类型,详细信息请查看Linux Kernel Documentation BPF Map。
本次我们将使用以下类型:
- BPF_MAP_TYPE_HASH
- 表示哈希表的Map。
- 可以存储哈希键和值对。
我们将使用数据包的源IP地址作为键,将丢弃的数据包数量作为值存储。
eBPF Map的创建
在eBPF程序中,可以如下定义eBPF Map。
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, uint32_t);
__type(value, uint64_t);
__uint(max_entries, 1024);
} icmp_count_map SEC(".maps");
type
: 指定Map的类型。key
: 指定键的类型。此次指定为uint32_t
,因为我们将使用IPv4地址作为键。value
: 指定值的类型。此次指定为uint64_t
,因为我们要存储丢弃的数据包数量。max_entries
: 指定Map中可以存储的最大条目数。- 对于BPF_MAP_TYPE_HASH,超过条目数的添加将导致错误。
icmp_count_map
: 指定Map的名称。SEC(".maps")
: 指定定义Map的节。
eBPF Map的操作
可以通过eBPF程序如下操作eBPF Map。
bpf_map_lookup_elem
: 从Map中获取与键对应的值。bpf_map_update_elem
: 向Map添加键和值对。bpf_map_delete_elem
: 从Map中删除与键对应的值。bpf_map_get_next_key
: 获取Map中的下一个键。
实际编写XDP程序
使用eBPF Map的程序
以下是使用eBPF Map的程序示例。
#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>
// eBPF Map的定义
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, uint32_t);
__type(value, uint64_t);
__uint(max_entries, 1024);
} icmp_count_map SEC(".maps");
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;
// 提取源IP地址
uint32_t src_ip = ip->saddr;
// 根据源IP地址查找icmp_count_map
uint64_t *count = bpf_map_lookup_elem(&icmp_count_map, &src_ip);
// 如果u64类型的指针不为NULL,则增加计数
if (count) {
__sync_fetch_and_add(count, 1);
} else {
// 如果为NULL,则添加到icmp_count_map
bpf_map_update_elem(&icmp_count_map, &src_ip, &(uint64_t){1}, BPF_ANY);
}
// 丢弃ICMP数据包
return XDP_DROP;
}
char _license[] SEC("license") = "MIT";
编译和加载
编译xdp.c,并加载XDP程序。
$ clang -O2 -target bpf -g -c xdp.c -o xdp.o
$ sudo ip link set dev eth0 xdp obj xdp.o sec xdp
从用户空间引用Map
设置pin以便可以引用map
为了从用户空间引用eBPF Map,我们需要对Map进行pin。
这样做能够使用户空间引用Map成为可能。
获取BPF程序的map id
$ sudo bpftool map 16: hash name icmp_count_map flags 0x0 key 4B value 8B max_entries 1024 memlock 86144B btf_id 25
- 使用
16
作为Map的id。
- 使用
设置Map的pin
$ sudo bpftool map pin id 16 /sys/fs/bpf/xdp/icmp_count_map
编写引用eBPF Map的用户空间程序
现在我们可以从用户空间引用存储在eBPF Map中的数据。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <linux/bpf.h>
#include <linux/if_link.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
int main(int argc, char **argv) {
int map_fd = bpf_obj_get("/sys/fs/bpf/xdp/icmp_count_map");
if (map_fd < 0) {
perror("bpf_obj_get");
return 1;
}
struct bpf_map_info map_info = {};
uint32_t info_len = sizeof(map_info);
if (bpf_obj_get_info_by_fd(map_fd, &map_info, &info_len)) {
perror("bpf_obj_get_info_by_fd");
return 1;
}
// 显示Map的信息
printf("Map name: %s\n", map_info.name);
printf("Map type: %d\n", map_info.type);
printf("Map key size: %d\n", map_info.key_size);
printf("Map value size: %d\n", map_info.value_size);
printf("Map max_entries: %d\n", map_info.max_entries);
// 获取所有条目
uint32_t key, next_key;
uint64_t value;
key = next_key = 0;
while (bpf_map_get_next_key(map_fd, &key, &next_key) == 0) {
key = next_key; // 使用更新后的键
if (bpf_map_lookup_elem(map_fd, &key, &value) == 0) {
// 将IP地址转换为字符串
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &key, ip_str, sizeof(ip_str));
printf("Key: %s, Value: %lu\n", ip_str, value);
}
key = next_key; // 为下一次循环做好准备
}
return 0;
}
bpf_obj_get
: 获取pin的Map的文件描述符。- 指定
/sys/fs/bpf/xdp/icmp_count_map
的pin Map。
- 指定
bpf_obj_get_info_by_fd
: 获取Map的信息。- 获取Map的名称、类型、键的大小、值的大小和最大条目数。
bpf_map_get_next_key
: 获取Map中的下一个键。key
: 当前键next_key
: 下一个键
bpf_map_lookup_elem
: 从Map中获取与键对应的值。
编译和运行
$ gcc -o map_info map_info.c -lbpf
$ sudo ./map_info
Map name: icmp_count_map
Map type: 1
Map key size: 4
Map value size: 8
Map max_entries: 1024
Key: 192.168.XX.10, Value: 320
Key: 192.168.XX.11, Value: 2307
这样,我们就可以查看XDP程序丢弃的ICMP数据包的源IP地址和计数。
清理工作
取消pin的Map
$ sudo rm -iv /sys/fs/bpf/xdp/icmp_count_map
卸载XDP程序
$ sudo ip link set dev eth0 xdp off
XDP的加载和引用Map的程序
每次通过ip命令加载XDP并设置pin以访问Map是很麻烦的,如果是常驻程序,当加载xdp程序时可以获取到Map的文件描述符,
这样可以在不设置pin的情况下访问Map。此外,在程序结束时卸载XDP程序,避免了忘记卸载或解除pin的情况。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <linux/bpf.h>
#include <linux/if_link.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include <unistd.h>
#include <net/if.h>
int main(int argc, char **argv) {
struct bpf_object *obj;
struct bpf_map *map;
struct bpf_program *prog;
struct bpf_link *link = NULL;
int map_fd, prog_fd;
int err;
if (argc != 2) {
fprintf(stderr, "Usage: %s <interface>\n", argv[0]);
return 1;
}
// 加载xdp程序
obj = bpf_object__open_file("./xdp.o", NULL);
if (libbpf_get_error(obj)) {
perror("Failed to open BPF object");
return 1;
}
if (bpf_object__load(obj)) {
perror("Failed to load BPF object");
return 1;
}
// 获取程序
prog = bpf_object__find_program_by_name(obj, "xdp_drop_icmp");
if (libbpf_get_error(prog)) {
perror("Failed to find BPF program by name");
return 1;
}
// 获取Map
map = bpf_object__find_map_by_name(obj, "icmp_count_map");
if (!map) {
perror("Failed to find BPF map by name");
return 1;
}
map_fd = bpf_map__fd(map);
if (map_fd < 0) {
perror("Failed to get map fd");
return 1;
}
// 附加XDP程序到eth0
const char* if_name = argv[1];
int ifindex = if_nametoindex(if_name);
if (ifindex == 0) {
perror("Failed to get interface index");
return 1;
}
link = bpf_program__attach_xdp(prog, ifindex);
if (libbpf_get_error(link)) {
perror("Failed to attach XDP program");
return 1;
}
// 无限循环,每秒显示一次Map的状态
while (1) {
// 分隔线
printf("==============================\n");
// 显示Map的信息
struct bpf_map_info map_info = {};
uint32_t info_len = sizeof(map_info);
if (bpf_obj_get_info_by_fd(map_fd, &map_info, &info_len)) {
perror("Failed to get map info");
return 1;
}
printf("Map name: %s\n", map_info.name);
printf("Map type: %d\n", map_info.type);
printf("Map key size: %d\n", map_info.key_size);
printf("Map value size: %d\n", map_info.value_size);
printf("Map max_entries: %d\n", map_info.max_entries);
// 获取所有条目
uint32_t key, next_key;
uint64_t value;
key = next_key = 0;
while (bpf_map_get_next_key(map_fd, &key, &next_key) == 0) {
key = next_key; // 使用更新后的键
if (bpf_map_lookup_elem(map_fd, &key, &value) == 0) {
// 将IP地址转换为字符串
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &key, ip_str, sizeof(ip_str));
printf("Key: %s, Value: %lu\n", ip_str, value);
}
key = next_key; // 为下一次循环做好准备
}
sleep(1); // 以1秒的间隔循环
}
return 0;
}
bpf_object__open_file
: 从文件中打开BPF对象。- 指定已构建的XDP对象文件。
bpf_object__load
: 加载BPF对象。- 加载BPF对象后,就可以获取程序和Map。
bpf_object__find_program_by_name
: 根据名称获取BPF程序。- 指定XDP程序的名称,通常是sec(“xdp”)指定后紧随的函数名。
bpf_object__find_map_by_name
: 根据名称获取BPF Map。- 指定Map的名称。
bpf_map__fd
: 获取Map的文件描述符。bpf_program__attach_xdp
: 将XDP程序附加到指定的接口。
编译和运行
编译
$ gcc -o map_monitor map_monitor.c -lbpf
运行
./map_monitor eth0 Map name: icmp_count_map Map type: 1 Map key size: 4 Map value size: 8 Map max_entries: 1024 Key: 192.168.XX.2, Value: 4 ============================== Map name: icmp_count_map Map type: 1 Map key size: 4 Map value size: 8 Map max_entries: 1024 Key: 192.168.XX.2, Value: 5 ^C
使用Ctrl+C退出。
退出时,XDP程序会被卸载。
总结
我们编写了一个用于统计XDP程序丢弃的ICMP数据包数量的程序。
通过使用Map,可以在eBPF程序和用户空间程序之间交换数据,
从而扩展XDP的应用范围。
最后补充
实际上,从性能考虑,应使用BPF_MAP_TYPE_PERCPU_HASH,而不是BPF_MAP_TYPE_HASH,
但为了简化,本次使用了BPF_MAP_TYPE_HASH。