使用XDP提取以太网头部时Perf Buffer返回固定MAC地址问题排查
我编写了一个基于XDP的程序,用于提取数据包中的以太网头部信息。使用bpf_trace_printk函数时一切正常,但切换为Perf Buffer后,打印出的MAC地址始终固定,如同硬编码一般。相关代码如下:
C 代码
struct data_t{ //ETH u16 type; unsigned char src_mac[ETH_ALEN]; unsigned char dst_mac[ETH_ALEN]; BPF_PERF_OUTPUT(packets); int xdp(struct xdp_md *ctx) { void *data = (void *)(long)ctx->data; void *data_end = (void *)(long)ctx->data_end; struct data_t packet = {}; if (is_tcp_packet(data, data_end)) { //eth struct ethhdr *eth = data; //bpf_trace_printk("tamos ai lol %p, %p", eth->h_source, eth->h_dest); packet.type = eth->h_proto; __builtin_memcpy(packet.src_mac, eth->h_source, ETH_ALEN); __builtin_memcpy(packet.dst_mac, eth->h_dest, ETH_ALEN); packets.perf_submit(ctx, &packet, sizeof(packet)); }
Python 代码
def callback(ctx, data, size): packet = bpf["packets"].event(data) result = bytes(packet.src_mac) src_mac = ':'.join(format(byte, '02x') for byte in result) result = bytes(packet.dst_mac) dst_mac = ':'.join(format(byte, '02x') for byte in result) print("TYPE=%d;SRC_MAC=%s;DST_MAC=%s" % (packet.type, src_mac, dst_mac)) bpf["packets"].open_perf_buffer(callback) try: print("Listening on IP:", ip) #bpf.trace_print() while True: bpf.perf_buffer_poll() except KeyboardInterrupt: print("\n Unloading") exit()
请问我是否存在操作错误?或是有更优的实现方式?
核心错误点
C代码结构体定义不完整
你的struct data_t定义缺少闭合的};,导致后续的BPF_PERF_OUTPUT(packets);被错误包含进结构体定义,彻底打乱了数据结构的内存布局。这是MAC地址输出固定的关键原因——Perf Buffer传输的根本不是你预期的数据包结构。缺少以太网头部边界检查
在访问以太网头部前,必须确保指针没有超出数据包的data_end范围,否则BPF验证器可能拒绝加载程序,或者导致非法内存访问,读取到错误的固定值。如果is_tcp_packet函数未做完整的边界校验,会引发这类问题。结构体对齐与Python解析匹配问题
C语言的结构体可能存在自动对齐(比如u16类型后可能填充2字节以满足4字节对齐),如果Python端的事件解析未对应调整,会导致MAC地址字段偏移错误,读取到固定的垃圾值。
修正后的代码
C代码修正
#include <linux/if_ether.h> #include <linux/bpf.h> #include <bpf/bpf_helpers.h> #include <linux/ip.h> #include <linux/tcp.h> struct data_t { __be16 type; // 用__be16保存网络字节序的协议类型 unsigned char src_mac[ETH_ALEN]; unsigned char dst_mac[ETH_ALEN]; }; // 补充结构体闭合括号 BPF_PERF_OUTPUT(packets); // 实现正确的TCP包检查与边界校验 static inline bool is_tcp_packet(void *data, void *data_end) { struct ethhdr *eth = data; // 检查以太网头部是否完整 if ((void *)eth + sizeof(*eth) > data_end) return false; // 检查是否为IP包 if (eth->h_proto != bpf_htons(ETH_P_IP)) return false; struct iphdr *ip = (void *)eth + sizeof(*eth); if ((void *)ip + sizeof(*ip) > data_end) return false; // 检查是否为TCP包 return ip->protocol == IPPROTO_TCP; } int xdp(struct xdp_md *ctx) { void *data = (void *)(long)ctx->data; void *data_end = (void *)(long)ctx->data_end; struct data_t packet = {}; if (is_tcp_packet(data, data_end)) { struct ethhdr *eth = data; // 再次确认边界(冗余但安全) if ((void *)eth + sizeof(*eth) > data_end) return XDP_PASS; packet.type = eth->h_proto; __builtin_memcpy(packet.src_mac, eth->h_source, ETH_ALEN); __builtin_memcpy(packet.dst_mac, eth->h_dest, ETH_ALEN); // 提交时使用固定的结构体大小,避免对齐问题 packets.perf_submit(ctx, &packet, sizeof(struct data_t)); } return XDP_PASS; // 必须返回合法的XDP动作 }
Python代码修正
from bcc import BPF # 替换为实际的C代码文本 bpf_text = """ // 上面修正后的C代码 """ bpf = BPF(text=bpf_text) ip = "127.0.0.1" # 替换为实际监听的IP def callback(ctx, data, size): packet = bpf["packets"].event(data) # 转换网络字节序的协议类型为主机字节序 proto = int.from_bytes(packet.type, byteorder='big') src_mac = ':'.join(f"{b:02x}" for b in packet.src_mac) dst_mac = ':'.join(f"{b:02x}" for b in packet.dst_mac) print(f"TYPE={proto};SRC_MAC={src_mac};DST_MAC={dst_mac}") bpf["packets"].open_perf_buffer(callback) try: print(f"Listening on IP: {ip}") while True: bpf.perf_buffer_poll() except KeyboardInterrupt: print("\nUnloading")
更优实现建议
显式控制结构体对齐
在C代码中使用__attribute__((packed))修饰结构体,强制取消自动对齐,确保内存布局与Python端完全一致:struct data_t __attribute__((packed)) { __be16 type; unsigned char src_mac[ETH_ALEN]; unsigned char dst_mac[ETH_ALEN]; };Python端显式绑定结构体
使用ctypes定义与C端完全匹配的结构体,避免自动解析的不确定性:import ctypes class DataT(ctypes.Structure): _fields_ = [ ("type", ctypes.c_uint16), ("src_mac", ctypes.c_ubyte * 6), ("dst_mac", ctypes.c_ubyte * 6) ] def callback(ctx, data, size): packet = ctypes.cast(data, ctypes.POINTER(DataT)).contents proto = ctypes.ntohs(packet.type) src_mac = ':'.join(f"{b:02x}" for b in packet.src_mac) dst_mac = ':'.join(f"{b:02x}" for b in packet.dst_mac) print(f"TYPE={proto};SRC_MAC={src_mac};DST_MAC={dst_mac}")高流量场景优化
对于高吞吐量场景,可手动创建BPF_MAP_TYPE_PERF_EVENT_ARRAY替代BPF_PERF_OUTPUT宏,精细控制内存分配;同时支持批量提交数据包,减少系统调用开销。调试辅助
保留bpf_trace_printk的调试代码,在Perf Submit前后打印MAC地址的原始值,确认数据在BPF侧的正确性,快速定位问题。
内容的提问来源于stack exchange,提问作者Afonso Pinto

