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

Linux eBPF TC出口程序无法解析ARP协议(0x0806)问题求助

TC egress eBPF解析ARP包报错:__sk_buff下eth->h_proto值异常

问题背景

需求为通过TC egress钩子结合eBPF,捕获网卡发送的ARP广播包中的目标IP地址。原有代码基于struct xdp_buff可正常解析报文,但适配struct __sk_buff后,解析以太网帧头的h_proto字段时得到错误的十六进制值,无法正确识别ARP协议(正常ARP的h_proto应为0x0806)。

相关代码与现象

1. 内核侧结构体定义

struct ethhdr {
    unsigned char   h_dest[ETH_ALEN];    /* 目标MAC地址 */
    unsigned char   h_source[ETH_ALEN];  /* 源MAC地址 */
    __be16          h_proto;             /* 上层协议类型 */
} __attribute__((packed));

struct arphdr {
    __be16          ar_hrd;      /* 硬件地址格式 */
    __be16          ar_pro;      /* 协议地址格式 */
    unsigned char   ar_hln;      /* 硬件地址长度 */
    unsigned char   ar_pln;      /* 协议地址长度 */
    __be16          ar_op;       /* ARP操作码 */
    unsigned char   ar_sha[ETH_ALEN]; /* 发送方MAC地址 */
    __be32          ar_sip;      /* 发送方IP地址 */
    unsigned char   ar_tha[ETH_ALEN]; /* 目标MAC地址 */
    __be32          ar_tip;      /* 目标IP地址 */
} __attribute__((packed));

2. 错误的eBPF解析代码

SEC("tc_egress")
int tc_egress_handler(struct __sk_buff *skb) {
    struct ethhdr *eth = (struct ethhdr *)skb->data;
    // 打印得到的h_proto值异常,并非0x0806
    bpf_trace_printk("eth proto: %x\n", eth->h_proto);

    if (eth->h_proto != htons(ETH_P_ARP)) {
        return TC_ACT_OK;
    }

    // 后续ARP解析逻辑...
    return TC_ACT_OK;
}

3. Go挂载代码片段

func attachTC(iface string) error {
    // 加载eBPF程序
    objs := bpfObjects{}
    if err := loadBpfObjects(&objs, nil); err != nil {
        return err
    }
    defer objs.Close()

    // 创建TC egress钩子
    qdisc, err := tc.NewQdisc(iface, tc.HandleIngress, tc.QdiscIngress)
    if err != nil {
        return err
    }
    defer qdisc.Close()

    // 挂载eBPF程序到egress
    filter := tc.NewFilter(qdisc, tc.HandleMin, tc.Bpf, tc.FilterEgress)
    filter.SetBpfFd(objs.TcEgressHandler.FD())
    return filter.Attrs().Commit()
}

4. 现象对比

  • tcpdump截图:可正常捕获到ARP广播包,以太网帧头协议字段为0x0806(ARP)。
  • bpf_trace_printk截图:打印的eth->h_proto为随机错误值(如0xffff、0x1234等),与实际报文不符。

问题原因

struct xdp_buff与struct __sk_buff的报文数据访问机制存在本质差异:

  1. XDP场景:xdp_buff->data指向线性连续的报文数据区,可直接通过结构体指针强制转换访问。
  2. TC eBPF场景:__sk_buff->data并非可靠的直接数据指针——TC处理的skb可能是**分散聚合(SKB)**结构,数据可能分散在多个内存页中,直接强制转换指针会访问无效内存,导致读取值异常。

此外,直接读取的h_proto是网络字节序,若未转换为主机字节序就打印,也可能出现显示异常,但此场景下核心问题是内存访问方式错误。

解决方案

修改eBPF代码,使用内核提供的bpf_skb_load_bytes()辅助函数安全读取skb数据,避免直接指针转换。

修改后的eBPF解析代码

#include <linux/if_ether.h>
#include <linux/arp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

SEC("tc_egress")
int tc_egress_handler(struct __sk_buff *skb) {
    struct ethhdr eth;
    // 安全加载以太网帧头数据
    if (bpf_skb_load_bytes(skb, 0, &eth, sizeof(struct ethhdr)) != 0) {
        return TC_ACT_OK;
    }

    // 转换为主机字节序后判断协议
    if (bpf_ntohs(eth.h_proto) != ETH_P_ARP) {
        bpf_trace_printk("Not ARP, proto: %x\n", bpf_ntohs(eth.h_proto));
        return TC_ACT_OK;
    }

    // 加载ARP头数据
    struct arphdr arp;
    if (bpf_skb_load_bytes(skb, sizeof(struct ethhdr), &arp, sizeof(struct arphdr)) != 0) {
        return TC_ACT_OK;
    }

    // 解析并打印目标IP地址
    __be32 target_ip = arp.ar_tip;
    bpf_trace_printk("ARP broadcast target IP: %x\n", bpf_ntohl(target_ip));

    return TC_ACT_OK;
}

额外注意事项

  1. 内核版本兼容:若使用5.10+内核,也可使用bpf_skb_pointer()获取线性区指针,但需先通过bpf_skb_is_gso()判断skb是否为线性,否则仍需用bpf_skb_load_bytes()。
  2. 字节序转换:所有网络字节序字段(如h_proto、ar_tip)需用bpf_ntohs()/bpf_ntohl()转换为主机字节序后再处理或打印。
  3. 边界检查:加载数据前需确保skb的len字段大于要读取的数据长度,避免越界读取(可通过skb->len判断)。

验证结果

修改代码后,bpf_trace_printk可正确打印eth->h_proto为0x0806,并能正常解析ARP包中的目标IP地址,与tcpdump捕获的报文一致。

内容的提问来源于stack exchange,提问作者niao ruo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 12:30:54