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的报文数据访问机制存在本质差异:
- XDP场景:
xdp_buff->data指向线性连续的报文数据区,可直接通过结构体指针强制转换访问。 - 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, ð, 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; }
额外注意事项
- 内核版本兼容:若使用5.10+内核,也可使用
bpf_skb_pointer()获取线性区指针,但需先通过bpf_skb_is_gso()判断skb是否为线性,否则仍需用bpf_skb_load_bytes()。 - 字节序转换:所有网络字节序字段(如
h_proto、ar_tip)需用bpf_ntohs()/bpf_ntohl()转换为主机字节序后再处理或打印。 - 边界检查:加载数据前需确保skb的
len字段大于要读取的数据长度,避免越界读取(可通过skb->len判断)。
验证结果
修改代码后,bpf_trace_printk可正确打印eth->h_proto为0x0806,并能正常解析ARP包中的目标IP地址,与tcpdump捕获的报文一致。
内容的提问来源于stack exchange,提问作者niao ruo
相关产品推荐
相关产品推荐

