如果修改了数据包 tcp 的目的端口号,也修改了对应 ipv4 头的目的 ip 地址,如何重新计算 tcp 头的 checksum ?
当同时修改 TCP 目的端口号 和 IPv4 头中的目的 IP 地址 时(例如在 NAT 或负载均衡场景中),由于 TCP 校验和(checksum)的计算包含了 伪头部(Pseudo Header),而伪头部中包含了源 IP、目的 IP、协议号和 TCP 长度,因此这两个字段的修改都会影响最终的 TCP 校验和。
此时,不能再仅对端口做简单的增量更新,而必须考虑 IP 地址变化带来的影响。幸运的是,可以使用 RFC 1624 的增量校验和思想,对两个变化进行 复合增量更新,而无需重新计算整个 TCP 数据的和。
TCP 校验和计算基于以下数据的反码和(one's complement sum):
•伪头部(Pseudo Header)(12 字节):•源 IP 地址(4 字节)•目的 IP 地址(4 字节) ← 被修改•零字节(1 字节)•协议号(1 字节,TCP 为 6)•TCP 长度(2 字节)•TCP 头部(含端口号等)•源端口(2 字节)•目的端口(2 字节) ← 被修改•其他字段(序列号、确认号、标志位等)•TCP 数据
最终校验和是使得所有这些 16 位字之和为 0xFFFF 的值。
如果原始校验和为 C,我们修改了两个 16 位字段(或可表示为 16 位和的字段),则新的校验和为:
其中 Δ 是所有修改字段的 反码差值之和。
但由于 IP 地址是 32 位,需拆分为两个 16 位字段处理。
假设:
•原目的 IP:old_dst_ip = A1A2 A3A4(十六进制,网络字节序)•新目的 IP:new_dst_ip = B1B2 B3B4•原目的端口:old_dst_port(16 位)•新目的端口:new_dst_port(16 位)•原 TCP 校验和:tcp_checksum(16 位)
在伪头部中,目的 IP 地址是 32 位,按网络字节序排列,可视为两个 16 位数:
•高 16 位:old_high = (A1A2),new_high = (B1B2)•低 16 位:old_low = (A3A4),new_low = (B3B4)
根据 RFC 1624,每个字段从 old 变为 new,对校验和的贡献是 (~old + new)。
因此总增量为:
注意:~old 是按位取反,所有运算为 反码算术加法(需处理进位)。
然后进行 回卷进位(end-around carry) 处理:
// 用 32 位累加器处理进位
uint32_t sum = (uint32_t)tcp_checksum;
sum += (uint32_t)(~old_high) + (uint32_t)new_high;
sum += (uint32_t)(~old_low) + (uint32_t)new_low;
sum += (uint32_t)(~old_dst_port) + (uint32_t)new_dst_port;
// 处理回卷进位:将高 16 位加到低 16 位,直到无进位
while (sum >> 16) {
sum = (sum & 0xFFFF) + (sum >> 16);
}
uint16_t new_tcp_checksum = (uint16_t)sum;别忘了,IPv4 头校验和也依赖于目的 IP 地址。IP 头校验和只覆盖 IP 头本身,不包含传输层数据。
IP 头校验和也可以增量更新:
// IP 校验和更新(假设原 IP 校验和为 ip_checksum)
uint32_t ip_sum = (uint32_t)ip_checksum + (uint32_t)(~old_dst_ip_high) + (uint32_t)new_dst_ip_high
+ (uint32_t)(~old_dst_ip_low) + (uint32_t)new_dst_ip_low;
while (ip_sum >> 16) {
ip_sum = (ip_sum & 0xFFFF) + (ip_sum >> 16);
}
ip_header->checksum = (uint16_t)ip_sum;假设:
•原目的 IP:192.168.1.1 → 0xC0A80101 → 高 0xC0A8,低 0x0101•新目的 IP:10.0.0.1 → 0x0A000001 → 高 0x0A00,低 0x0001•原目的端口:5000 → 0x1388•新目的端口:80 → 0x0050•原 TCP 校验和:0x1234
计算:
sum = 0x1234
+ (~0xC0A8 + 0x0A00) // IP 高 16 位
+ (~0x0101 + 0x0001) // IP 低 16 位
+ (~0x1388 + 0x0050) // 端口即:
sum = 0x1234
+ (0x3F57 + 0x0A00) // ~0xC0A8 = 0x3F57
+ (0xFEFE + 0x0001) // ~0x0101 = 0xFEFE
+ (0xEC77 + 0x0050) // ~0x1388 = 0xEC77
sum = 0x1234 + 0x4957 + 0xFEFF + 0xEC87
= 0x1234 + 0x4957 = 0x5B8B
+ 0xFEFF = 0x15B8A → 回卷: 0x5B8A + 1 = 0x5B8B
+ 0xEC87 = 0x14812 → 回卷: 0x4812 + 1 = 0x4813所以新 TCP 校验和为 0x4813。
当同时修改 TCP 目的端口 和 IPv4 目的 IP 地址 时,重新计算 TCP 校验和的方法如下:
1.将目的 IP 地址拆分为两个 16 位字段(高 16 位和低 16 位)。2.对每个被修改的 16 位字段(IP 高、IP 低、目的端口),计算增量:(~old_value + new_value)。3.将这些增量与原 TCP 校验和相加。4.对结果进行 回卷进位处理,直到结果在 16 位范围内。5.得到的新值即为更新后的 TCP 校验和。6.额外:使用类似方法更新 IPv4 头校验和(仅针对 IP 头字段)。
这种方法正是 RFC 1624 所倡导的 增量校验和(Incremental Checksum) 的典型应用。
对比维度 | RFC 1624 增量校验和更新法 | DPDK rte_ipv4_udptcp_cksum 重算法 |
|---|---|---|
性能(小包) | ⭐⭐⭐⭐⭐ 极高 O(1)仅需计算修改字段的补码差值,不访问 payload | ⭐⭐⭐⭐ 中高需遍历 TCP 首部 + 整个 payload,但 DPDK 使用 SIMD 加速(如 SSE/AVX),对小包很快 |
性能(大包) | ⭐⭐⭐⭐⭐ 几乎不变仍 O(1) | ⭐⭐ 较低与 payload 大小成正比 O(n),大包(如 64KB jumbo frame)需大量内存访问和求和,开销显著上升 |
适用场景 | ✅ 单字段或少量字段修改(如 port、TTL、DSCP 等字段的修改) | ✅ 任意修改(IP、端口、payload 等)通用性强,适合复杂处理 |
实现复杂度 | ❌ 高,需精确处理反码、进位、字节序、奇数字节对齐等;易出错 | ✅ 低,调用标准 API,逻辑清晰,DPDK 已封装细节 |
可维护性与安全性 | ❌ 差,代码晦涩,调试困难;若旧字段读错或旧校验和错误,结果错误且难以察觉 | ✅ 好,基于原始数据重新计算,不依赖中间状态,更可靠 |
健壮性 | ❌ 弱,依赖原始校验和正确性和字段值准确性 | ✅ 强,可验证原始报文是否合法(通过重算后比对) |
硬件加速支持 | ❌ 无,纯逻辑运算,无法利用 SIMD | ✅ 强,rte_raw_cksum 可使用 SIMD 指令(如 sse, avx)优化求和过程 |
内存访问开销 | ✅ 极低,仅访问修改字段和旧校验和 | ⚠️ 高,需访问:IP 伪首部 + TCP 首部 + 整个 TCP payload大包时可能引起 cache miss 和内存带宽瓶颈 |
缓存局部性 | ✅ 好,只访问极小区域 | ⚠️ 差,大 payload 可能超出 L1/L2 缓存,导致性能下降 |
校验和验证能力 | ❌ 无,不能用于验证原始报文完整性 | ✅ 有,可用于校验接收到的报文是否出错 |
•RFC 1624: Computation of the Internet Checksum via Incremental Update•Intel DPDK Programmer’s Guide