前不久,很多人问我有没有用过xdpdump,它是什么原理。
当然,当时我是没有用过的,也就没有多说,不过我答应大家一旦我了解了之后,肯定会第一时间给大家介绍。
最近在写一些测试小程序的时候,偶然间也有了XDP抓包的需求,也就顺便熟悉了一下xdpdump,最终,我自己写了一个简单的,主要是阐明它的原理。
当然了,经理没有看这篇文章的必要。
在XDP抓包不能使用tcpdump,因为tcpdump是基于PACKET套接字的,而PACKET套接字是运行在Linux内核协议栈的,XDP在内核协议栈之前,所以tcpdump够不到它,也就无法抓取它的数据包。因此,需要一个xdpdump。
xdpdump已经存在了,但是xdpdump并不是一个类似tcpdump的工具,它只是一个说法,并没有统一的实现,我Google了一下,发现xdpdump的实现有很多版本:
之所以会这样,在于XDP还没有实现一个统一类似处理PACKET套接字的 ptype_all框架 (至少在目前没有这种机制)。
这很容易理解,因为XDP本身就是网卡强相关的,不适合做generic操作。所以说,如果需要xdpdump这样的功能,除了掌握上面列的几家的现成工具外,最好的方式无外乎:
现在让我们开始。
实现xdpdump之前,必须要解决的一个问题就是:
这是必须的,因为抓包只是一个旁路功能,它不能影响到既有的XDP上eBPF程序的运行,如果当前某网卡的XDP运行着一个eBPF程序,我们希望的是xdpdump和它一起工作,而不是替换它。
我们假设当前现有的eBPF程序是test_echo.c,如下所示:
#include
#include
#include "bpf_helpers.h"
SEC("xdp_echo")
int xdp_echo_prog(struct xdp_md *ctx)
{
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
int in_index = ctx->ingress_ifindex;
char info_fmt[] = "echo to %d \n";
if (data + sizeof(struct ethhdr) > data_end) {
return XDP_DROP;
}
bpf_trace_printk(info_fmt, sizeof(info_fmt), in_index);
return bpf_redirect(in_index, 0);
}
char _license[] SEC("license") = "GPL";
非常简单的一个eBPF程序,它将一个数据包原路反射回去。
很显然,我们用tcpdump无法抓取到达对应网卡的数据包。我们现在的任务是实现一个xdpdump,它可以抓到到达对应网卡并发射回去的数据包。
不得不介绍一下eBPF的两类机制:
知道这些就够了。接下来我们就用尾调用和PIN map来将xdpdump的eBPF程序和原始test_echo这个eBPF程序串联起来,让它们一起工作。
首先,我们看xdpdump的eBPF程序,即test_dump.c,代码如下:
#include
#include
#include
#include
#include
#include
#include "bpf_helpers.h"
#include "bpf_endian.h"
struct bpf_elf_map {
__u32 type;
__u32 size_key;
__u32 size_value;
__u32 max_elem;
__u32 flags;
__u32 id;
__u32 pinning;
};
#define PIN_GLOBAL_NS 2
// 保存下一个eBPF程序,即本文中的test_echo程序,提供给尾调用。
struct bpf_elf_map SEC("maps") next_prog_map = {
.type = BPF_MAP_TYPE_PROG_ARRAY,
.size_key = sizeof(u32),
.size_value = sizeof(u32),
.pinning = PIN_GLOBAL_NS,
.max_elem = 1,
};
// 保存抓取的数据包体,这仅仅用协议元组数据来模拟。
struct packet {
unsigned int src;
unsigned int dst;
unsigned short l3proto;
unsigned short l4proto;
unsigned short sport;
unsigned short dport;
};
// 保存抓取数据包事件信息
struct bpf_elf_map SEC("maps") event_map = {
.type = BPF_MAP_TYPE_PERF_EVENT_ARRAY,
.size_key = sizeof(u32),
.size_value = sizeof(u32),
.pinning = PIN_GLOBAL_NS,
.max_elem = 128,
};
SEC("xdp_dump")
int xdp_dump_prog(struct xdp_md *ctx)
{
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
struct packet p = {};
if (data + sizeof(struct ethhdr) > data_end) {
return XDP_DROP;
}
p.l3proto = bpf_htons(eth->h_proto);
if (p.l3proto == ETH_P_IP) {
struct iphdr *iph;
iph = data + sizeof(struct ethhdr);
if (iph + 1 > data_end)
return XDP_DROP;
p.src = iph->saddr;
p.dst = iph->daddr;
p.l4proto = iph->protocol;
p.sport = p.dport = 0;
if (iph->protocol == IPPROTO_TCP) {
struct tcphdr *tcph;
tcph = data + sizeof(struct ethhdr) + sizeof(struct iphdr);
if (tcph + 1 > data_end)
return XDP_DROP;
p.sport = tcph->source;
p.dport = tcph->dest;
} else if (iph->protocol == IPPROTO_UDP) {
struct udphdr *udph;
udph = data + sizeof(struct ethhdr) + sizeof(struct iphdr);
if (udph + 1 > data_end)
return XDP_DROP;
p.sport = udph->source;
p.dport = udph->dest;
}
// 事件上报给xdpdump抓包进程
bpf_perf_event_output(ctx, &event_map, BPF_F_CURRENT_CPU, &p, sizeof(p));
}
// 尾调用,调用正常的test_echo eBPF程序
bpf_tail_call(ctx, &next_prog_map, 0);
// 如果没有attach别的eBPF程序,则直接PASS
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
依然是在源码树的samples/bpf目录下完成编译,得到两个.o文件备用:
-rw-r--r-- 1 root root 11864 12月 24 16:27 test_dump.o
-rw-r--r-- 1 root root 5648 12月 24 09:20 test_echo.o
OK,现在让我们加载test_dump.o到enp0s9网卡:
root@zhaoya-VirtualBox:~/bpf# ip -force link set dev enp0s9 xdp obj ./test_dump.o sec xdp_dump
此时,我们将在文件系统中看到两个PIN map:
root@zhaoya-VirtualBox:~/bpf# ll /sys/fs/bpf/xdp/globals/
total 0
drwx------ 2 root root 0 12月 24 15:25 ./
drwx------ 3 root root 0 12月 24 15:25 ../
-rw------- 1 root root 0 12月 24 15:25 event_map
-rw------- 1 root root 0 12月 24 15:25 next_prog_map
root@zhaoya-VirtualBox:~/bpf#
接下来要做的事情,任务很明确,即将test_echo.o这个eBPF程序,灌进next_prog_map的index=0的位置,这显然需要一个用户态程序来完成,即update_prog.c,代码如下:
#include
#include
#include
#include
#include "bpf_util.h"
static int progmap_fd;
int main(int argc, char **argv)
{
int idx = 0;
int opt = 1;
char *mapfile;
struct bpf_object *obj;
struct bpf_prog_load_attr prog_load_attr = {
.prog_type = BPF_PROG_TYPE_XDP,
};
int prog_fd;
opt = atoi(argv[1]);
mapfile = argv[2]; // 获取全局可见的PIN map文件位置
prog_load_attr.file = argv[3];
progmap_fd = bpf_obj_get(mapfile);
if (opt == 0) {
bpf_map_delete_elem(progmap_fd, &idx);
return 0;
}
// 载入eBPF程序
if (bpf_prog_load_xattr(&prog_load_attr, &obj, &prog_fd)) {
return 1;
}
bpf_map_update_elem(progmap_fd, &idx, &prog_fd, 0);
return 0;
}
我们将其编译成update_prog可执行程序,将test_echo.o灌入:
root@zhaoya-VirtualBox:~/bpf# ./update_prog 1 /sys/fs/bpf/xdp/globals/next_prog_map ./test_echo.o
原本能ping通enp0s9地址1.1.1.1,现在ping不通了,数据包被反射:
04:06:30.004904 IP 1.1.1.2 > 1.1.1.1: ICMP echo request, id 6242, seq 97, length 64
04:06:30.005273 IP 1.1.1.2 > 1.1.1.1: ICMP echo request, id 6242, seq 97, length 64
04:06:31.004684 IP 1.1.1.2 > 1.1.1.1: ICMP echo request, id 6242, seq 98, length 64
04:06:31.005394 IP 1.1.1.2 > 1.1.1.1: ICMP echo request, id 6242, seq 98, length 64
删除test_echo.o,重新可以ping通:
root@zhaoya-VirtualBox:~/bpf# ./update_prog 0 /sys/fs/bpf/xdp/globals/next_prog_map
可以在ping的机器上抓包确认:
04:09:47.234037 IP 1.1.1.1 > 1.1.1.2: ICMP echo reply, id 6242, seq 294, length 64
04:09:48.236846 IP 1.1.1.2 > 1.1.1.1: ICMP echo request, id 6242, seq 295, length 64
04:09:48.237430 IP 1.1.1.1 > 1.1.1.2: ICMP echo reply, id 6242, seq 295, length 64
04:09:49.238854 IP 1.1.1.2 > 1.1.1.1: ICMP echo request, id 6242, seq 296, length 64
这说明我们的串联两个eBPF程序成功了。
注意最初通过iproute2加载的test_dump.o这个eBPF程序,其中已经把数据包抓取并上报了,现在只需要最后一道工序,即实现用户态的xdpdump了。
这也不难,我们用perf event采集机制,xdpdump.c的代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define CPUS 4
struct packet {
unsigned int src;
unsigned int dst;
unsigned short l3proto;
unsigned short l4proto;
unsigned short sport;
unsigned short dport;
};
struct perf_event_data {
struct perf_event_header header;
unsigned long long ts;
unsigned int size;
struct packet p;
};
static enum bpf_perf_event_ret print_packet(struct perf_event_header *hdr, void *fn)
{
struct perf_event_data *data = (struct perf_event_data *)hdr;
struct packet p = data->p;
unsigned long long ts = data->ts;
char src[16], dst[16];
char l3proto[8], l4proto[8];
unsigned short sport = 0, dport = 0;
// 直接打印数据包协议元数据,正常应该是利用libpcap接口来处理的。
switch (p.l3proto) {
case ETH_P_IP:
strcpy(l3proto, "IP");
inet_ntop(AF_INET, &p.src, src, 16);
inet_ntop(AF_INET, &p.dst, dst, 16);
break;
default:
sprintf(l3proto, "%04x", p.l3proto);
}
switch (p.l4proto) {
case IPPROTO_TCP:
strcpy(l4proto, "TCP");
sport = ntohs(p.sport);
dport = ntohs(p.dport);
break;
case IPPROTO_UDP:
strcpy(l4proto, "UDP");
sport = ntohs(p.sport);
dport = ntohs(p.dport);
break;
case IPPROTO_ICMP:
strcpy(l4proto, "ICMP");
break;
default:
strcpy(l4proto, "Unknown");
}
printf("%lld.%06lld %s:%d > %s:%d > %s %s\n", ts/1000000000, (ts%1000000000)/1000, src, sport, dst, dport, l3proto, l4proto);
return LIBBPF_PERF_EVENT_CONT;
}
int main(int argc, char **argv)
{
static struct perf_event_mmap_page *buffer[CPUS];
int eventmap_fd, he;
int perf_fds[CPUS];
void *tmp = NULL;
unsigned long len = 0;
int i;
struct pollfd fds[CPUS];
struct perf_event_attr attr = {
.sample_type = PERF_SAMPLE_RAW | PERF_SAMPLE_TIME,
.type = PERF_TYPE_SOFTWARE,
.config = PERF_COUNT_SW_BPF_OUTPUT,
.wakeup_events = 1,
};
eventmap_fd = bpf_obj_get(argv[1]);
for (i = 0; i < CPUS; i++) {
he = sys_perf_event_open(&attr, -1, i, -1, 0);
ioctl(he, PERF_EVENT_IOC_ENABLE, 0);
buffer[i] = mmap(NULL, 8192, PROT_READ | PROT_WRITE, MAP_SHARED, he, 0);
bpf_map_update_elem(eventmap_fd, &i, &he, BPF_ANY);
perf_fds[i] = he;
}
for (i = 0; i < CPUS; i++) {
fds[i].fd = perf_fds[i];
fds[i].events = POLLIN;
}
while (1) {
poll(fds, CPUS, 0);
for (i = 0; i < CPUS; i++)
bpf_perf_event_read_simple(buffer[i], 8192, 4096, &tmp, &len, print_packet, NULL);
}
return 0;
}
编译成xdpdump(同样在源码树的samples/bpf目录下)之后,我们看看效果:
OK,很像那么回事。
然而,缺失了很多东西,需要补充:
…
再说下eBPF之好,eBPF可以实现一些内核空间才能的策略,且 不用再担心系统panic了。
此外,再说句题外话:
本文中我的代码均没有按照每行80字符的规矩,因为我觉得那是历史的遗毒。如今显示器的分辨率都这么高了,类似Linux社区这种还要以此为编码规范,我不能理解。其实,在很多方面,Linux内核社区这种都被过度无脑神话了,很多方面如果你仔细看,它就是垃圾!
- 每行80字符规定
- 邮件发送接收竟然如此麻烦
- …
浙江温州皮鞋湿,下雨进水不会胖。