You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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()

请问我是否存在操作错误?或是有更优的实现方式?


问题分析与解决方案

核心错误点

  1. C代码结构体定义不完整
    你的struct data_t定义缺少闭合的};,导致后续的BPF_PERF_OUTPUT(packets);被错误包含进结构体定义,彻底打乱了数据结构的内存布局。这是MAC地址输出固定的关键原因——Perf Buffer传输的根本不是你预期的数据包结构。

  2. 缺少以太网头部边界检查
    在访问以太网头部前,必须确保指针没有超出数据包的data_end范围,否则BPF验证器可能拒绝加载程序,或者导致非法内存访问,读取到错误的固定值。如果is_tcp_packet函数未做完整的边界校验,会引发这类问题。

  3. 结构体对齐与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")

更优实现建议

  1. 显式控制结构体对齐
    在C代码中使用__attribute__((packed))修饰结构体,强制取消自动对齐,确保内存布局与Python端完全一致:

    struct data_t __attribute__((packed)) {
        __be16 type;
        unsigned char src_mac[ETH_ALEN];
        unsigned char dst_mac[ETH_ALEN];
    };
    
  2. 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}")
    
  3. 高流量场景优化
    对于高吞吐量场景,可手动创建BPF_MAP_TYPE_PERF_EVENT_ARRAY替代BPF_PERF_OUTPUT宏,精细控制内存分配;同时支持批量提交数据包,减少系统调用开销。

  4. 调试辅助
    保留bpf_trace_printk的调试代码,在Perf Submit前后打印MAC地址的原始值,确认数据在BPF侧的正确性,快速定位问题。


内容的提问来源于stack exchange,提问作者Afonso Pinto

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 16:14:59