eBPF的透明眼睛
网络可观测性与流量镜像的艺术
◆ ◆ ◆
你有没有在电影里见过审讯室里的那面"单向玻璃"?
一侧是审讯室,嫌疑人和警察正在谈话;另一侧是观察室,侦探和分析师静静地看着这一切。被观察的人毫不知情——他们的每一个微表情、每一句话,都被记录下来。关键是:这面玻璃不会干扰审讯室里的任何事情,观察者只是看,不介入。
这就是 eBPF网络可观测性的精髓。
在传统的网络监控中,你要么在链路上插入一个探针(改变了流量路径),要么在应用层抓包(已经晚了很多步)。而eBPF就像那面单向玻璃——它站在内核网络栈的最深处,看到每一个数据包的完整生命历程,却不改变任何一个字节,不增加任何可见的延迟。
今天,我们要打开这扇"透明眼睛",看清数据包在内核里的旅行。
一个数据包从网卡到应用程序,要经历以下阶段:
| [XDP Hook] |
| [TC Ingress Hook] |
| [Socket Filter] |
每一个 Hook 点都是一扇"单向玻璃"——你可以在这里观察数据包,决定它的命运,或者仅仅记录它的信息。
| XDP | ||
| TC | ||
| Socket Filter |
无论在哪个 Hook 点,eBPF 程序访问数据包都通过 struct __sk_buff(XDP 用 struct xdp_md):
struct__sk_buff { __u32 len; /* 数据包总长度 */ __u32 pkt_type; /* 包类型(单播/广播/多播) */ __u32 protocol; /* 协议类型(ETH_P_IP等) */ __u32 ifindex; /* 网络接口索引 */ __u32 cb[5]; /* 控制块(可自定义使用) */ // ... 更多字段 }; |
在 eBPF 程序中解析以太网帧 → IP头 → TCP/UDP头,是网络可观测性的基础操作:
#include<linux/bpf.h> #include<linux/if_ether.h> #include<linux/ip.h> #include<linux/tcp.h> SEC("xdp") intobserve_packets(struct xdp_md *ctx) { // 1. 获取数据包边界 void *data = (void *)(long)ctx->data; void *data_end = (void *)(long)ctx->data_end; // 2. 解析以太网头 struct ethhdr *eth = data; if ((void *)(eth + 1) > data_end) /* 边界检查 */ return XDP_PASS; // 3. 只处理IPv4 if (eth->h_proto != bpf_htons(ETH_P_IP)) return XDP_PASS; // 4. 解析IP头 struct iphdr *ip = (void *)(eth + 1); if ((void *)(ip + 1) > data_end) return XDP_PASS; // 5. 记录源IP和目标IP __u32 src_ip = ip->saddr; __u32 dst_ip = ip->daddr; // 6. 解析TCP头 if (ip->protocol == IPPROTO_TCP) { struct tcphdr *tcp = (void *)ip + (ip->ihl * 4); if ((void *)(tcp + 1) > data_end) return XDP_PASS; __u16 src_port = bpf_ntohs(tcp->source); __u16 dst_port = bpf_ntohs(tcp->dest); } return XDP_PASS; /* 放行数据包 */ } |
⚠️ 关键点:每次解析都必须做边界检查( |
实现"透明眼睛"的关键是高效地把内核看到的数据传给用户态——这正是 BPF_MAP_TYPE_PERF_EVENT_ARRAY 的用武之地:
// 定义事件结构(内核与用户态共用) structpacket_event { __u32 src_ip; __u32 dst_ip; __u16 src_port; __u16 dst_port; __u8 protocol; /* 6=TCP, 17=UDP */ __u32 pkt_len; }; // 定义 Perf Event Map struct { __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); __uint(key_size, sizeof(int)); __uint(value_size, sizeof(int)); } events SEC(".maps"); // 发送事件到用户态(无锁,高性能) bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &evt, sizeof(evt)); |
你知道 tcpdump 背后其实也用了 eBPF(的前身 BPF)吗?
tcpdump 的工作原理是在 Socket 层挂载一个 Socket Filter,让内核在每个数据包到达时运行过滤规则,只把匹配的包复制一份给 tcpdump 进程。这样:
✅ 零拷贝:不匹配的包完全不复制,极大节省性能 ✅ 内核过滤:过滤逻辑在内核执行,用户态只收到目标数据包 ✅ 不影响流量:原始数据包继续正常传输 |
现代 eBPF 的 TC Hook 更进一步,可以在任意方向(ingress/egress)、任意接口上挂载,实现更灵活的流量镜像。
#include<linux/bpf.h> #include<linux/if_ether.h> #include<linux/ip.h> #include<linux/tcp.h> #include<bpf/bpf_helpers.h> #include<bpf/bpf_endian.h> // 统计Map:目标端口 → 连接次数 struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 64); __type(key, __u16); /* 目标端口 */ __type(value, __u64); /* 包计数 */ } port_stats SEC(".maps"); // 事件Map:用于向用户态实时推送 structtcp_event { __u32 src_ip; __u32 dst_ip; __u16 src_port; __u16 dst_port; __u8 tcp_flags; }; struct { __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); __uint(key_size, sizeof(int)); __uint(value_size, sizeof(int)); } tcp_events SEC(".maps"); SEC("xdp") inttcp_monitor(struct xdp_md *ctx) { void *data = (void *)(long)ctx->data; void *data_end = (void *)(long)ctx->data_end; // 解析以太网头 struct ethhdr *eth = data; if ((void *)(eth + 1) > data_end) return XDP_PASS; if (eth->h_proto != bpf_htons(ETH_P_IP)) return XDP_PASS; // 解析IP头 struct iphdr *ip = (void *)(eth + 1); if ((void *)(ip + 1) > data_end) return XDP_PASS; if (ip->protocol != IPPROTO_TCP) return XDP_PASS; // 解析TCP头 struct tcphdr *tcp = (void *)ip + ip->ihl * 4; if ((void *)(tcp + 1) > data_end) return XDP_PASS; __u16 dst_port = bpf_ntohs(tcp->dest); // 更新端口统计 __u64 *cnt = bpf_map_lookup_elem(&port_stats, &dst_port); if (cnt) { __sync_fetch_and_add(cnt, 1); /* 原子递增 */ } // 发送TCP事件到用户态 struct tcp_event evt = { .src_ip = ip->saddr, .dst_ip = ip->daddr, .src_port = bpf_ntohs(tcp->source), .dst_port = dst_port, .tcp_flags = (((__u8 *)tcp)[13]), /* TCP flags字节 */ }; bpf_perf_event_output(ctx, &tcp_events, BPF_F_CURRENT_CPU, &evt, sizeof(evt)); return XDP_PASS; } char _license[] SEC("license") = "GPL"; |
#include<stdio.h> #include<arpa/inet.h> #include<bpf/libbpf.h> #include"tcp_monitor.skel.h" // Perf事件回调:打印TCP连接信息 staticvoidhandle_event(void *ctx, int cpu, void *data, __u32 size) { struct tcp_event *evt = data; char src[INET_ADDRSTRLEN], dst[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &evt->src_ip, src, sizeof(src)); inet_ntop(AF_INET, &evt->dst_ip, dst, sizeof(dst)); printf("[TCP] %s:%d → %s:%d flags=0x%02x\n", src, evt->src_port, dst, evt->dst_port, evt->tcp_flags); } intmain() { // 1. 打开并加载eBPF程序(libbpf骨架API) struct tcp_monitor_bpf *skel = tcp_monitor_bpf__open_and_load(); // 3. 初始化Perf Buffer struct perf_buffer *pb = perf_buffer__new( bpf_map__fd(skel->maps.tcp_events), 8, /* 每CPU 8页缓冲 */ handle_event, NULL, NULL, NULL ); printf("开始监控TCP流量(Ctrl+C退出)...\n"); // 4. 事件循环:持续读取数据 while (1) { perf_buffer__poll(pb, 100); /* 100ms超时 */ } perf_buffer__free(pb); tcp_monitor_bpf__destroy(skel); return0; } |
# 编译eBPF内核程序 clang -O2 -target bpf \ -I/usr/include/bpf \ -c tcp_monitor.bpf.c \ -o tcp_monitor.bpf.o # 生成libbpf骨架头文件 bpftool gen skeleton tcp_monitor.bpf.o > tcp_monitor.skel.h # 编译用户态程序 gcc -O2 tcp_monitor.c -o tcp_monitor -lbpf # 挂载XDP程序并运行 ip link set dev eth0 xdp obj tcp_monitor.bpf.o sec xdp sudo ./tcp_monitor |
开始监控TCP流量(Ctrl+C退出)... [TCP] 192.168.1.100:54321 → 10.0.0.1:80 flags=0x02 (SYN) [TCP] 10.0.0.1:80 → 192.168.1.100:54321 flags=0x12 (SYN+ACK) [TCP] 192.168.1.100:54321 → 10.0.0.1:443 flags=0x02 (SYN) [TCP] 192.168.1.100:55000 → 10.0.0.1:22 flags=0x02 (SYN → SSH连接) |
bpftool map dump name port_stats key: 00 50 value: 00 00 00 00 00 00 01 5e # 端口80, 350个包 key: 01 bb value: 00 00 00 00 00 00 02 3a # 端口443, 570个包 key: 00 16 value: 00 00 00 00 00 00 00 03 # 端口22, 3个包 |
回到我们的"单向玻璃"隐喻。
传统网络监控就像在大街上架设摄像头——你能记录到人们的行为,但摄像头的存在本身也改变了人们的行为。而eBPF的观察是真正透明的:
🔹 XDP层的观察:在数据包刚刚离开网卡驱动、还没进入内核网络栈时就完成记录——就像在快递进入分拣中心前就扫描了包裹外皮 🔹 零侵入:被监控的应用程序、网络协议、数据包本身,完全感知不到这双眼睛的存在 🔹 全局视野:不论是容器内的流量、加密前的明文、还是系统调用层的数据,eBPF都能看到——这是传统工具无法企及的视角 |
这种能力带来了网络可观测性的范式革命:
| Cilium | |
| Falco | |
| Pixie |
哲学层面的思考:任何系统都有其不可见的盲区。eBPF做的事情,不是在盲区里安装探针(会改变系统行为),而是让整个内核网络栈本身变成透明的。这是观察哲学的一次跨越——从"侵入式测量"到"无损观测"。
◆ ◆ ◆
1. 内核网络栈有三个主要eBPF挂载点:XDP(最早)、TC(流量控制层)、Socket Filter(套接字层) 2. 数据包解析必须做边界检查,这是eBPF verifier的安全保障,不能省略 3. Perf Event Array是从内核向用户态高效传递网络事件的核心机制 4. tcpdump的底层原理就是Socket Filter——eBPF让这个机制变得更强大、更灵活 5. 实战中,组合 XDP + Map + Perf Event 可以构建实时网络流量监控器 |
◆ ◆ ◆
下一节预告
第(十六)节:eBPF的守门人——Security & LSM Hook