GSO/TSO场景下eBPF tc添加的PPPoE头能否在分片中正确复制?
问题
我正在使用eBPF和tc在出口侧为转发及本地生成的数据包添加PPPoE头。由于GSO/TSO机制,遇到了超过MTU大小的数据包,观察到skb->gso_size=1452、skb->gso_segs=3、skb->len=4410等参数。我担心在tc程序中插入的MAC和PPPoE头,能否在GSO/TSO生成的每个分片中被正确包含。
我理解分片发生在IP层,而PPPoE工作在链路层,每个分片应是完整的链路层帧,但不确定GSO是否会在每个分片中复制MAC和PPPoE头。若确实如此,我该如何处理每个分片的PPPoE头len字段?或者有什么特定方法能确保每个分片被正确处理?
以下是我使用的eBPF代码:
SEC("tc") int pppoe_egress(struct __sk_buff *skb) { #define BPF_LOG_TOPIC "pppoe_egress" void *data_end = (void *)(long)skb->data_end; void *data = (void *)(long)skb->data; u32 pkt_sz = skb->len - 14; if (pkt_sz > pppoe_mtu) { bpf_log_info("egress package too large size: %u", pkt_sz); return TC_ACT_SHOT; } struct ethhdr *eth = (struct ethhdr *)(data); if ((void *)(eth + 1) > data_end) { bpf_log_info("package size smaller than ethhdr"); return TC_ACT_SHOT; } if (eth->h_proto != ETH_IPV4 && eth->h_proto != ETH_IPV6) { bpf_log_info("egress eth proto is error: %x", eth->h_proto); return TC_ACT_PIPE; } u32 offset = 14; u8 protocol = 0; u16 mss_value = 0; u16 ppp_proto = ETH_PPP_IPV4; // DECAP support since Linux kernel 6.3 u64 adj_room_flag = BPF_F_ADJ_ROOM_ENCAP_L3_IPV4; if (eth->h_proto == ETH_IPV6) { ppp_proto = ETH_PPP_IPV6; adj_room_flag = BPF_F_ADJ_ROOM_ENCAP_L3_IPV6; struct ipv6hdr *iph6; if (VALIDATE_READ_DATA(skb, &iph6, offset, sizeof(*iph6))) { return TC_ACT_SHOT; } protocol = iph6->nexthdr; offset = offset + 40; mss_value = pppoe_mtu - 40 - 20; } else { struct iphdr *iph; if (VALIDATE_READ_DATA(skb, &iph, offset, sizeof(*iph))) { return TC_ACT_SHOT; } protocol = iph->protocol; offset = offset + (iph->ihl * 4); mss_value = pppoe_mtu - (iph->ihl * 4) - 20; } if (protocol == IPPROTO_TCP) { mss_clamp(skb, offset, mss_value); } u16 l2_proto = bpf_htons(0x8864); bpf_skb_store_bytes(skb, 12, &l2_proto, sizeof(u16), 0); int result = bpf_skb_adjust_room(skb, 8, BPF_ADJ_ROOM_MAC, adj_room_flag); if (result) { bpf_log_info("egress adjust room error %d", result); return TC_ACT_SHOT; } struct pppoe_header pppoe = { .version_and_type = 0x11, .code = 0x00, .session_id = bpf_htons(session_id), .length = bpf_htons(pkt_sz + 2), .protocol = ppp_proto, }; bpf_skb_store_bytes(skb, sizeof(struct ethhdr), &pppoe, sizeof(struct pppoe_header), 0); return TC_ACT_PIPE; #undef BPF_LOG_TOPIC }
解决方案
1. GSO/TSO分片的链路层头复制逻辑
Linux内核的GSO/TSO机制在分片时,会自动复制整个链路层头部(包括你添加的MAC和PPPoE头)到每个分片。GSO处理上层IP/TCP分片,而链路层头属于skb的head区域,分片过程中会完整复制该区域到每个新生成的skb,无需手动干预。
2. PPPoE头length字段的处理
当前代码使用原大包的长度计算length,会导致分片后每个PPPoE头的长度值错误。解决思路是:
- 不要在tc阶段直接固定
length值,或改为基于当前skb的实际IP payload长度计算; - 优先让GSO分片在PPPoE封装前完成,确保每个分片的IP包大小符合PPPoE MTU限制,此时
length字段可以直接设置为对应IP包长度+2(PPP协议字段长度)。
3. 代码调整建议
(1)移除大包丢弃逻辑,允许GSO包通过
原代码中if (pkt_sz > pppoe_mtu)直接丢弃大包,会阻止GSO分片。替换为调整GSO参数:
// 替换原有的pkt_sz检查逻辑 if (skb->gso_size) { // 根据IP版本调整GSO分片大小,适配PPPoE MTU if (eth->h_proto == ETH_IPV4) { skb->gso_size = pppoe_mtu - (iph->ihl * 4); } else { skb->gso_size = pppoe_mtu - 40; } }
(2)修正PPPoE length字段计算
基于当前skb的实际长度计算,而非原大包长度:
// 替换原有的length赋值 u32 ip_payload_len = skb->len - sizeof(struct ethhdr); struct pppoe_header pppoe = { .version_and_type = 0x11, .code = 0x00, .session_id = bpf_htons(session_id), .length = bpf_htons(ip_payload_len + 2), .protocol = ppp_proto, };
(3)添加GSO修正标志
调用bpf_skb_adjust_room时加入BPF_F_ADJ_ROOM_FIX_GSO,让内核自动修正GSO参数(需内核5.10+支持):
int result = bpf_skb_adjust_room(skb, 8, BPF_ADJ_ROOM_MAC, adj_room_flag | BPF_F_ADJ_ROOM_FIX_GSO);
(4)调整TCP MSS clamp值
确保TCP MSS适配PPPoE链路:
// IPv4 MSS = PPPoE MTU - IP头 - TCP头 mss_value = pppoe_mtu - (iph->ihl * 4) - 20; // IPv6 MSS = PPPoE MTU - IPv6头 - TCP头 mss_value = pppoe_mtu - 40 - 20;
4. 额外注意事项
- 若内核版本低于5.10,无法使用
BPF_F_ADJ_ROOM_FIX_GSO,需手动调整skb->gso_segs参数; - 确认
pppoe_mtu设置为PPPoE链路的实际MTU(通常为1492,即以太网MTU 1500减去8字节PPPoE头)。
内容的提问来源于stack exchange,提问作者Sean1179
相关产品推荐
相关产品推荐

