eBPF修改IP和TCP端口后校验值偏大1的问题排查
eBPF修改数据包IP与TCP端口后校验和始终偏大1的问题
我编写了一个可修改数据包IP地址和TCP端口的简易eBPF程序,但使用辅助函数更新校验和时,计算出的结果始终比正确值大1。tcpdump输出如下:
00:28:00.743620 IP (tos 0x0, ttl 64, id 20245, offset 0, flags [DF], proto TCP (6), length 60, bad cksum 67f0 (->67ef)!) 192.168.1.1.60758 > 192.168.1.102.5002: Flags [S], cksum 0xb5de (incorrect -> 0xb5dd), seq 1988705508, win 64240, options [mss 1460,sackOK,TS val 4170540693 ecr 0,nop,wscale 7], length 0 00:28:01.776693 IP (tos 0x0, ttl 64, id 20246, offset 0, flags [DF], proto TCP (6), length 60) 192.168.1.1.60758 > 192.168.1.102.5002: Flags [S], cksum 0xb1d5 (incorrect -> 0xb1d4), seq 1988705508, win 64240, options [mss 1460,sackOK,TS val 4170541726 ecr 0,nop,wscale 7], length 0 00:28:01.776900 IP (tos 0xc0, ttl 64, id 27418, offset 0, flags [none], proto ICMP (1), length 88) 192.168.1.101 > 192.168.1.1: ICMP redirect 192.168.1.102 to host 192.168.1.102, length 68 IP (tos 0x0, ttl 63, id 20246, offset 0, flags [DF], proto TCP (6), length 60) 192.168.1.1.60758 > 192.168.1.102.5002: Flags [S], cksum 0xb1d5 (incorrect -> 0xb1d4), seq 1988705508, win 64240, options [mss 1460,sackOK,TS val 4170541726 ecr 0,nop,wscale 7], length 0 00:28:02.800554 IP (tos 0x0, ttl 64, id 20247, offset 0, flags [DF], proto TCP (6), length 60) 192.168.1.1.60758 > 192.168.1.102.5002: Flags [S], cksum 0xadd5 (incorrect -> 0xadd4), seq 1988705508, win 64240, options [mss 1460,sackOK,TS val 4170542750 ecr 0,nop,wscale 7], length 0
我的eBPF代码如下:
#include <linux/bpf.h> #include <linux/if_ether.h> #include <linux/ip.h> #include <linux/udp.h> #include <linux/tcp.h> #include <linux/in.h> // For IPPROTO_TCP and IPPROTO_UDP #include <linux/pkt_cls.h> // For TC_ACT_OK and other TC actions #include <bpf/bpf_helpers.h> #include <bpf/bpf_endian.h> #define OLD_IP 0xC0A80165 /* 192.168.1.101 in hex */ #define NEW_IP 0xC0A80166 /* 192.168.1.102 in hex */ #define NEW_PORT 5002 /* Destination port in decimal */ SEC("tc") int modify_packet(struct __sk_buff *skb) { void *data = (void *)(long)skb->data; void *data_end = (void *)(long)skb->data_end; // Check Ethernet header struct ethhdr *eth = data; if ((void *)(eth + 1) > data_end) return TC_ACT_OK; // Pass the packet if bounds are exceeded // Only handle IPv4 packets if (eth->h_proto != bpf_htons(ETH_P_IP)) return TC_ACT_OK; // Check IP header struct iphdr *ip = (struct iphdr *)(eth + 1); if ((void *)(ip + 1) > data_end) return TC_ACT_OK; // Only handle TCP packets if (ip->protocol != IPPROTO_TCP) return TC_ACT_OK; // Check if destination IP is 192.168.1.101 if (ip->daddr != bpf_htonl(OLD_IP)) return TC_ACT_OK; // Save old destination IP for checksum calculation __u32 old_ip = ip->daddr; __u32 new_dst_ip = bpf_htonl(NEW_IP); ip->daddr = new_dst_ip; // Update to the new IP //!!!!!!!!!!!!!! IP CHECKSUM - IDK IF VALID BUT WORKS __u32 csum_diff = bpf_csum_diff(&old_ip, sizeof(old_ip), &new_dst_ip, sizeof(new_dst_ip),0); __u32 new = (~((~ip->check) + csum_diff)); __u32 second = (new>>8) & 0xF; if(second > 0) { second -= 1; } new &= ~(0xF << 8); new |= (second << 8); ip->check = new; //////////////////////////////////////////////////////////// // Handle TCP header struct tcphdr *tcp = (void *)ip + (ip->ihl * 4); if ((void *)(tcp + 1) > data_end) return TC_ACT_OK; // Save old port for checksum calculation __u16 old_port = tcp->dest; __u16 new_port = bpf_htons(NEW_PORT); tcp->dest = new_port; // Calculate TCP pseudo-header checksum bpf_l4_csum_replace(skb, offsetof(struct tcphdr, check), old_ip, new, BPF_F_PSEUDO_HDR); bpf_l4_csum_replace(skb, offsetof(struct tcphdr, check), old_port, new_port, 0); // AGAIN -1 TO DO __u16 hej = tcp->check; //////////////////////////////////// return TC_ACT_OK; // Pass the modified packet } char LICENSE[] SEC("license") = "GPL";
核心疑问:为何使用eBPF辅助函数计算出的校验和仍与正确值不匹配?
问题原因与修复方案
1. IP校验和计算错误
你手动调整IP校验和的逻辑完全不必要且易出错。bpf_csum_diff本身可以正确计算校验和的增量更新,正确的IP校验和更新方式是直接利用返回值替换原校验和:
// 替换原有的手动IP校验和计算代码 ip->check = bpf_csum_diff(&old_ip, sizeof(old_ip), &new_dst_ip, sizeof(new_dst_ip), ~ip->check);
bpf_csum_diff的最后一个参数是初始校验和的补码,传入~ip->check即可让辅助函数直接计算出正确的新校验和,无需手动处理进位或减1操作。
2. TCP校验和更新参数错误
你的TCP校验和更新代码存在致命错误:调用bpf_l4_csum_replace更新伪头部IP时,第四个参数传入了IP校验和的临时变量new,而非实际的新目的IPnew_dst_ip。这个错误直接导致TCP校验和计算偏差。
修正后的TCP校验和更新代码:
// 修正后的TCP校验和更新 bpf_l4_csum_replace(skb, offsetof(struct tcphdr, check), old_ip, new_dst_ip, BPF_F_PSEUDO_HDR); bpf_l4_csum_replace(skb, offsetof(struct tcphdr, check), old_port, new_port, 0);
BPF_F_PSEUDO_HDR标志告诉辅助函数同时考虑TCP伪头部的变化(此处为目的IP修改),第二个调用则仅更新TCP头部内的目的端口变化。
完整修正后的代码
#include <linux/bpf.h> #include <linux/if_ether.h> #include <linux/ip.h> #include <linux/udp.h> #include <linux/tcp.h> #include <linux/in.h> #include <linux/pkt_cls.h> #include <bpf/bpf_helpers.h> #include <bpf/bpf_endian.h> #define OLD_IP 0xC0A80165 /* 192.168.1.101 in hex */ #define NEW_IP 0xC0A80166 /* 192.168.1.102 in hex */ #define NEW_PORT 5002 /* Destination port in decimal */ SEC("tc") int modify_packet(struct __sk_buff *skb) { void *data = (void *)(long)skb->data; void *data_end = (void *)(long)skb->data_end; struct ethhdr *eth = data; if ((void *)(eth + 1) > data_end) return TC_ACT_OK; if (eth->h_proto != bpf_htons(ETH_P_IP)) return TC_ACT_OK; struct iphdr *ip = (struct iphdr *)(eth + 1); if ((void *)(ip + 1) > data_end) return TC_ACT_OK; if (ip->protocol != IPPROTO_TCP) return TC_ACT_OK; if (ip->daddr != bpf_htonl(OLD_IP)) return TC_ACT_OK; __u32 old_ip = ip->daddr; __u32 new_dst_ip = bpf_htonl(NEW_IP); ip->daddr = new_dst_ip; // 正确更新IP校验和 ip->check = bpf_csum_diff(&old_ip, sizeof(old_ip), &new_dst_ip, sizeof(new_dst_ip), ~ip->check); struct tcphdr *tcp = (void *)ip + (ip->ihl * 4); if ((void *)(tcp + 1) > data_end) return TC_ACT_OK; __u16 old_port = tcp->dest; __u16 new_port = bpf_htons(NEW_PORT); tcp->dest = new_port; // 正确更新TCP校验和 bpf_l4_csum_replace(skb, offsetof(struct tcphdr, check), old_ip, new_dst_ip, BPF_F_PSEUDO_HDR); bpf_l4_csum_replace(skb, offsetof(struct tcphdr, check), old_port, new_port, 0); return TC_ACT_OK; } char LICENSE[] SEC("license") = "GPL";
原理说明
- IP校验和:IPv4校验和是基于头部的16位补码和,
bpf_csum_diff会计算新旧字段的校验和差值,自动处理进位和补码转换,直接返回正确的新校验和。 - TCP校验和:TCP校验和覆盖TCP头部、数据以及伪头部(包含源/目的IP、协议号、TCP长度)。修改目的IP和端口后,必须通过
bpf_l4_csum_replace分别更新这两部分的校验和,且参数必须准确对应新旧值。
内容的提问来源于stack exchange,提问作者marcfranc
相关产品推荐
相关产品推荐

