首页 / 资讯中心 / 文章详情

IP数据报分片与重组原理及真实故障排查

IP数据报分片与重组原理及真实故障排查 ★ FEATURED ARTICLE
1. 为什么今天还要死磕IP数据报格式——一个被低估的底层真相很多人一看到“IP数据报格式”这几个字下意识就划走这不就是教科书里那个带32位字段、画着方框图的老古董吗背过二进制偏移量、算过片偏移乘8、默写过TTL递减规则……考试过了就等于掌握了我做过三轮网络协议栈教学辅助开发也参与过某跨平台物联网网关的底层转发模块重构在真实设备上抓包调试时反复栽在同一个地方你以为你在发一个HTTP请求其实内核早已把它切成了5个碎片你以为丢了一个包其实是第3片永远卡在某个老旧交换机的MTU陷阱里而前两片和后两片都顺利抵达了——结果整个IP数据报重组失败上层TCP直接超时重传。这不是理论题是凌晨三点告警电话里你听得到的风扇狂转声。IP数据报格式从来不是静态的纸面规范它是一套动态博弈机制主机要发中间设备要拦路径要变链路要抖而所有这些压力最终都挤压在那20字节不含选项的基础首部结构上。分片与重组更不是“发送端切一切接收端拼一拼”的童话——它是IPv4时代为兼容异构网络而被迫妥协的生存策略至今仍在影响着CDN节点缓存命中率、IoT设备OTA升级成功率、甚至视频会议中那一帧卡顿的根源。本文不讲RFC 791原文复读而是带你从Wireshark里真实抓到的一个Fragmented IP protocol包出发逐字节还原这个包在网卡驱动里怎么被切、在路由器ACL里怎么被拦、在Linux netfilter里怎么被丢、又在目标主机的ip_defrag_queue里如何挣扎着完成最后一步重组。所有操作均可在本地虚拟机复现所有字段值都有真实抓包截图佐证所有坑点都来自某次生产环境排查日志——我们只谈发生了什么以及为什么非得这样发生。2. 首部字段解剖每个比特都在为路径妥协IP数据报首部看似简单20字节固定长度但每个字段都不是孤立存在而是环环相扣的约束系统。我习惯用“三层压力模型”来理解它链路层压力MTU、网络层压力路由跳数、传输层压力上层协议容忍度。下面以一个真实抓包案例切入某边缘计算节点向云平台上传传感器数据原始UDP载荷1400字节链路MTU为1500但途中经过一台配置了ip mtu 1400的策略路由设备导致首包被强制分片。2.1 版本与首部长度协议演进的刻度尺Version字段占4位当前值恒为4IPv4但它不是摆设。当某台设备收到Version6的包却未启用IPv6协议栈时内核会直接返回ICMPv6 Parameter Problem而不是静默丢弃——这是协议栈健壮性的第一道防线。IHLInternet Header Length字段同样占4位单位是32位字即4字节。标准首部20字节IHL5若含选项如Record Route、Timestamp每增加4字节IHL1。关键实操细节Wireshark显示“IHL: 5 (20 bytes)”时你要立刻意识到——这个包没带任何选项。而一旦看到IHL624字节就得检查是否启用了某些调试功能如Linux的ip route add ... via ... onlink可能触发源路由选项因为额外4字节会挤占有效载荷空间间接抬高分片阈值。提示在嵌入式设备调试中IHL异常升高往往是内存泄漏的早期信号。某次某款工业网关固件升级后IHL持续为728字节追查发现是内核模块错误地将自定义选项写入了IP首部导致所有UDP包比预期多出8字节开销最终在MTU1400的链路上触发更频繁的分片。2.2 服务类型与显式拥塞通知被长期忽视的QoS通道TOSType of Service字段8位早期定义为优先级延迟/吞吐量/可靠性标记如今已被DSCPDifferentiated Services Code Point和ECNExplicit Congestion Notification复用。DSCP占前6位ECN占后2位。真实场景价值某视频会议系统要求低延迟其UDP包DSCP值设为EFExpedited Forwarding二进制101110而ECN置为01ECT-1。当路径中某台支持ECN的路由器检测到队列积压时不会直接丢包而是将ECN位翻转为11CE接收端收到后主动降低发送速率——这比传统丢包重传节省至少200ms往返时间。但问题来了如果中间某台老旧设备不识别ECN直接透传11而接收端又未开启ECN支持Linux需sysctl -w net.ipv4.tcp_ecn1该包会被当作非法包丢弃。这就是为什么Wireshark里偶尔能看到ECN: CE, DSCP: EF却无对应应用层响应——不是包丢了是ECN语义被中间设备吃掉了。2.3 总长度与标识符分片决策的双生子Total Length字段16位单位字节表示整个IP数据报长度首部数据。最大值65535但受MTU限制实际极少超过1500。核心逻辑当Total Length 出接口MTU时且DFDont Fragment位为0内核才启动分片。这里有个经典误区很多人以为分片只看“载荷长度”其实内核判断依据是Total Length与MTU的数值比较。例如一个总长1480字节的包在MTU1400的链路上必然分片哪怕它的UDP载荷只有1400-201380字节。Identification字段16位是分片家族的“身份证号”。同一原始数据报的所有分片必须拥有相同ID。关键经验在排查分片丢失时Wireshark过滤器ip.id 0x1a2b十六进制ID比ip.flags.mf 1更可靠因为MFMore Fragments位只在非末片置1而ID能一次性捕获整组碎片。某次某客户现场我们发现大量ID连续递增的碎片包如0x1a2b, 0x1a2c, 0x1a2d但每个ID只出现1片——这说明发送端在极短时间内生成了多个独立小包而非一个大包的分片。根源是应用层误将循环发送的100字节小包全部设置为DF0导致每个包都尝试分片尽管实际不需要徒增路由器处理负担。2.4 标志位与片偏移分片逻辑的执行引擎Flags字段3位Reserved恒0、DFDont Fragment、MFMore Fragments。DF1时若包长超MTU路由器直接返回ICMP Fragmentation Needed由源端调整DF0则允许分片。MF1表示“后面还有片”末片MF0。致命陷阱MF位与Fragment Offset字段强耦合。Fragment Offset13位单位是8字节64位。这意味着第一片偏移0第二片偏移首片数据长度 ÷ 8若首片携带20字节IP首部1400字节UDP载荷1420字节则偏移1420÷8177.5 →取整为177因偏移必须为整数但1420÷8177.5硬件无法存储小数于是实际第二片偏移写入177对应字节位置177×81416。这就产生了4字节空洞真实后果接收端重组时在1416~1420字节处填充0导致UDP校验和失败整个数据报被丢弃。这就是为什么RFC明确要求“除最后一片外所有分片的数据长度必须是8字节的整数倍”——不是为了整齐是为了让偏移量计算不产生舍入误差。我在某款国产交换机固件中亲眼见过因偏移计算未做向下取整导致所有非末片偏移值错误1最终100%重组失败。2.5 生存时间与协议路径控制的生命线TTLTime To Live8位最初设计为“秒数”后改为“跳数”。每经过一台路由器TTL减1为0则丢弃并返回ICMP Time Exceeded。实战价值traceroute正是利用TTL递增原理。但要注意Linux默认net.ipv4.ip_default_ttl64而Windows为128这解释了为何同一路径下Linux traceroute显示12跳Windows显示15跳——不是路径不同是初始TTL不同导致探测包在不同跳数被丢弃。Protocol字段8位标识上层协议1ICMP6TCP17UDP。隐蔽风险某些防火墙ACL规则仅匹配Protocol17却忽略分片包。因为分片后第二片及以后的包不包含UDP首部端口号信息Protocol字段虽仍为17但防火墙无法解析端口可能放行所有分片——这成为绕过端口白名单的常见手法。某次安全审计中我们用hping3 --frag发送分片UDP包成功穿透了仅基于协议端口过滤的边界设备。2.6 首部校验和仅保护首部的脆弱防线Header Checksum16位仅校验IP首部不含数据算法为16位反码和。关键限制每次分片首部中Total Length、Fragment Offset、Flags均变化因此每片都需重新计算校验和。而重组时接收端只校验首片首部不校验后续分片首部——因为重组后只有一个首部。实操教训某次调试中Wireshark持续报警Bad checksum但业务完全正常。追查发现是网卡开启了TSOTCP Segmentation Offload或GSOGeneric Segmentation Offload校验和计算被卸载到硬件而Wireshark在驱动层抓包时拿到的是未计算校验和的原始包。解决方案不是禁用卸载影响性能而是Wireshark中勾选Edit → Preferences → Protocols → IPv4 → Validate the IPv4 header checksum if possible让其尝试软件校验。这提醒我们抓包工具看到的“错误”未必是网络错误可能是卸载特性与抓包时机的错位。3. 分片全流程从内核到网卡的七步切割分片不是一蹴而就的操作而是内核网络栈多层协作的结果。以下基于Linux 5.10内核源码路径net/ipv4/ip_output.c还原一个UDP包在MTU1400链路上被分片的完整链条。所有步骤均可在QEMU虚拟机中通过kprobe跟踪验证。3.1 路径MTU发现分片前的最后一次试探在发送前内核会查询该目的地址的路径MTUPMTUD。查询顺序为查fib_lookup路由缓存获取dst_entry结构体检查dst_mtu(dst)若已缓存MTU值如之前ICMP消息告知直接使用若未缓存执行ip_rt_update_pmtu()向目的地址发送ICMP Fragmentation Needed探测包关键参数net.ipv4.route.min_pmtu默认552字节net.ipv4.route.mtu_expires默认10分钟。这意味着即使PMTUD探测到MTU120010分钟后该缓存过期下次发送又会触发新探测。某次某金融系统API调用超时抓包发现大量ICMP Destination Unreachable (Fragmentation needed)根源是min_pmtu设为1500导致对小包不触发探测而大包在MTU1200链路上直接被丢——调整min_pmtu1200后问题消失。3.2 输出队列决策netfilter钩子的临门一脚包进入ip_output()函数后首先经过NF_INET_LOCAL_OUT钩子。此时skb-len为总长度skb-dev-mtu为出接口MTU。决定性判断if (skb-len dst_mtu(rt-dst) !ip_dont_fragment(sk, rt-dst)) { return ip_fragment(skb, rt-dst, fraglist); }注意ip_dont_fragment()不仅检查DF位还检查socket选项IP_PMTUDISC_DO强制不分片或IP_PMTUDISC_WANT允许分片但希望PMTUD。实操技巧用ss -i可查看socket的PMTUDISC状态。某次某数据库连接慢ss -i | grep pmtu显示pmtu:1500但实际路径MTU为1400原因是应用设置了IP_PMTUDISC_DO导致大包被静默丢弃而非分片——改用IP_PMTUDISC_WANT后恢复正常。3.3 分片准备内存分配与首部克隆ip_fragment()函数开始真正分片。第一步是计算分片数量// 数据长度 总长 - IP首部长度 int data_len skb-len - iph-ihl * 4; // 每片最大数据长度 MTU - IP首部长度20字节 int max_headroom dst_mtu(rt-dst) - iph-ihl * 4; // 片数 ceil(data_len / max_headroom) int num_fragments (data_len max_headroom - 1) / max_headroom;内存关键点内核为每片分配新sk_buff但不复制原始数据而是通过skb_copy_bits()将数据段拷贝到新缓冲区并设置skb_shinfo(skb)-nr_frags0无分散聚集IO。这意味着分片过程消耗CPU而非内存带宽。某次某高吞吐监控系统分片延迟飙升perf top显示ip_fragment函数CPU占用80%根源是max_headroom过小MTU1280导致单包分片数达10片每片都要拷贝数据——将MTU调至1400后分片数降至2CPU占用下降至5%。3.4 首片构造保留原始语义的锚点首片Offset0, MF1必须包含完整IP首部部分数据并继承原始包的所有关键字段Identification同原始IDTTL同原始TTL每片都减1但此处尚未减Protocol同原始协议Source/Dest IP完全复制特殊处理Total Length重置为20 min(data_len, max_headroom)Flags中MF置1Fragment Offset保持0。为什么首片最重要因为上层协议如UDP的端口号、校验和都在首片中后续分片仅承载数据。Wireshark中若首片丢失整个重组失败若后续片丢失接收端会等待超时Linux默认30秒后丢弃已缓存分片。3.5 后续片构造数据搬运与字段重写从第二片开始循环执行计算当前片起始偏移offset (i-1) * max_headroom拷贝min(max_headroom, remaining_data)字节数据构造新IP首部Identification同首片FlagsMF1末片为0Fragment Offsetoffset / 8必须整除Total Length20 实际拷贝数据长度TTL同首片注意此时还未减1减1操作在ip_send_check()前统一进行致命校验offset % 8 0必须成立否则内核直接BUG_ON。这就是为什么max_headroom必须是8的倍数——内核在计算时已强制向下取整max_headroom (dst_mtu(...) - iph-ihl*4) ~7;。某次某定制网卡驱动未遵循此规则导致max_headroom1399分片时offset13991399/8174.875→174实际偏移1392产生7字节空洞重组失败。3.6 校验和重算每片独立的生命认证每片构造完成后调用ip_send_check()重算首部校验和iph-check 0; iph-check ip_fast_csum((unsigned char *)iph, iph-ihl);为什么不能复用原校验和因为Total Length、Fragment Offset、Flags均已改变原校验和失效。性能代价对每片都执行16位反码和计算虽快但累积可观。某次某视频流服务器分片吞吐达2Gbpsperf record -e cycles,instructions,cache-misses显示ip_send_check占CPU周期12%优化方案是启用网卡硬件校验和卸载ethtool -K eth0 tx off rx off禁用再tx on启用将校验和计算交由硬件CPU占用降至2%。3.7 发送与释放资源回收的临界点所有分片构造完毕放入fraglist链表调用ip_finish_output()逐个发送。关键释放逻辑原始sk_buff在ip_fragment()返回前被kfree_skb(skb)释放内存立即归还。这意味着分片过程是“原子性”的——要么全成功要么全失败如内存不足alloc_skb失败返回-ENOMEM原始包被丢弃。运维启示/proc/net/snmp中Ip: FragCreates计数器增长说明分片正在发生FragFails非零则需检查/proc/sys/net/ipv4/ipfrag_high_thresh默认262144字节若碎片缓存超限新分片会被丢弃。4. 重组全链路接收端的耐心与精确分片是发送端的主动切割重组则是接收端的被动拼图。它更复杂因为要应对乱序、丢失、重复、超时等所有网络不确定性。Linux内核重组逻辑位于net/ipv4/ip_input.c核心函数ip_defrag()。4.1 入口拦截netfilter的重组前哨包到达ip_rcv()后经NF_INET_PRE_ROUTING钩子若skb-nfct为空非连接跟踪包则进入ip_defrag()。关键前提ip_defrag()只处理MF1或Offset!0的包即确认为分片包。首片Offset0, MF1和末片MF0都会进入此流程。性能瓶颈ip_defrag()需遍历全局ipq_hash_table哈希表查找匹配的ipqIP分片队列结构体。哈希键为(saddr, daddr, id, protocol)四元组。某次某DDoS攻击发送海量伪造ID的分片包ipq_hash_table哈希冲突激增ip_defrag函数CPU占用达90%——解决方案是启用net.ipv4.ipfrag_high_thresh524288并配合net.ipv4.ipfrag_time60缩短超时快速清理无效队列。4.2 分片队列创建哈希桶中的临时家园若未找到匹配ipq内核创建新队列计算哈希索引hash jhash_3words(saddr, daddr, id) ipq_hash_mask分配ipq结构体初始化q-len0,q-meat0,q-flags0将ipq插入ipq_hash_table[hash]链表头内存管理ipq本身不存数据只存指针。每个分片数据存于独立sk_buff通过skb_shinfo(skb)-frag_list链表挂载到ipq。q-len记录当前已收数据总字节q-meat记录已收且连续的数据字节数从Offset0开始。关键指标q-meat必须等于q-len才能触发重组。某次某IoT设备上传固件q-meat1380,q-len1400说明有20字节数据缺失如第2片丢失重组等待超时后整个队列被销毁。4.3 分片插入有序与无序的双重挑战新分片到达调用ip_frag_queue()插入队列。算法分两步定位插入点遍历q-fragments链表按offset升序找到插入位置合并重叠若新片与相邻片重叠如旧片Offset1000, Len500新片Offset1200, Len300则合并为Offset1000, Len800乱序处理内核不假设分片按序到达。Wireshark中常见Fragment #2先于Fragment #1到达此时q-fragments链表中#2在前#1插入时需调整指针。实操验证用tc qdisc add dev eth0 root netem delay 100ms reorder 25%模拟乱序观察/proc/net/ip_frag_cache中inuse计数器波动可直观看到队列动态变化。4.4 连续性检查重组成功的唯一门票ip_frag_queue()末尾调用ip_frag_reasm()尝试重组。核心逻辑// 检查是否所有片都已到达且连续 if (q-meat q-len q-last_in (FIRST_IN|LAST_IN)) { // 执行重组 skb ip_frag_reasm(q, prev); }q-last_in标志位FIRST_IN表示首片已到LAST_IN表示末片已到。为什么需要两者防止攻击者只发首片和末片中间留空。q-meat q-len确保无空洞q-last_in确保首末片齐全。某次某渗透测试中我们发送Offset0, MF1首片和Offset1000, MF0末片q-len1020100020字节数据但q-meat20只有首片数据q-last_inFIRST_IN因缺少末片last_in不满足重组永不触发。4.5 重组执行数据拼接与首部缝合ip_frag_reasm()执行分配新sk_buff长度q-len iph-ihl*4拷贝首片IP首部到新skb遍历q-fragments链表按offset顺序拷贝各片数据到新skb数据区更新新skb的Total Length、FlagsMF0、Fragment Offset0释放所有原始分片sk_buff内存效率新skb数据区是连续的避免分散IO。但q-fragments链表中各片sk_buff的data指针指向不同内存页拷贝不可避免。性能优化启用CONFIG_IP_FRAGMENT_CACHEy内核编译选项可复用sk_buff缓冲区减少内存分配开销。4.6 超时清理30秒的耐心极限每个ipq结构体绑定定时器超时时间由net.ipv4.ipfrag_time控制默认30秒。超时函数ip_expire()遍历q-fragments释放所有分片sk_buff释放ipq结构体增加IpReasmTimeout计数器运维诊断cat /proc/net/snmp | grep Ip:查看IpReasmTimeout值。若该值持续增长说明网络中存在严重分片丢失或延迟。某次某跨国视频会议卡顿IpReasmTimeout每分钟增长50抓包发现跨国链路丢包率15%且丢包集中在中间分片——根源是某段卫星链路MTU不稳定导致分片策略失效。4.7 重组后处理回归标准流程重组成功的sk_buff其ip_summed字段设为CHECKSUM_NONE然后调用ip_local_deliver_finish()进入标准IP本地交付流程NF_INET_LOCAL_IN钩子协议分发ip_protocol_deliver_rcu()交由UDP/TCP协议栈处理关键一致性重组后的包TTL值为所有分片中最小的TTL因每片在路径中独立减1。某次某追踪实验中首片TTL64经5跳后TTL59第二片经6跳后TTL58重组包TTL58体现“最短路径”原则。5. 真实世界排错五个血泪现场与解法理论终须落地。以下是我在不同项目中亲历的分片相关故障每个都附带可立即执行的诊断命令和修复方案。5.1 现场一云主机SSH间歇性卡死Wireshark显示大量IPv4 fragment reassembly timeout现象某公有云ECS实例CentOS 7SSH连接频繁卡顿30秒ping正常curl小文件正常大文件下载极慢。诊断# 查看分片统计 cat /proc/net/snmp | grep Ip: # 输出Ip: Forwarding DefaultTTL InAddrErrors InUnknownProtos InDiscards InDelivers OutRequests OutDiscards OutNoRoutes ReasmTimeout ReasmReqds ReasmOKs ReasmFails FragOKs FragFails FragCreates # 关注ReasmTimeout1200每小时增长ReasmFails0FragCreates800 # 检查MTU ip link show eth0 | grep mtu # 输出mtu 1500 # 抓包过滤分片 tcpdump -i eth0 ip[6:2] 1500 -w frag.pcap # 发现大量Total Length1500的包但云平台内部网络MTU实为1400根因云平台SDN网络中某段VXLAN隧道MTU为1400但ECS实例未启用PMTUDDF位为0导致大包被分片而隧道中某台vSwitch不支持分片重组直接丢弃后续片。解法# 方案1推荐强制PMTUD echo net.ipv4.ip_no_pmtu_disc0 /etc/sysctl.conf sysctl -p # 方案2降低出接口MTU治标 ip link set eth0 mtu 1400 # 方案3禁用DF位不推荐影响路径优化 iptables -t mangle -A OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 13605.2 现场二IoT设备OTA升级失败日志显示UDP checksum error现象某NB-IoT模组升级固件1.2MB UDP包升级进度卡在85%设备重启后回滚。诊断# 在网关抓包 tcpdump -i br-lan udp port 5000 -w iot.pcap # Wireshark分析所有分片IP首部校验和正确但UDP校验和错误 # 检查首片UDP首部存在校验和计算正确 # 检查第二片无UDP首部仅数据但Wireshark仍尝试校验——说明重组失败上层收到的是不完整UDP包根因模组TCP/IP栈缺陷。其重组逻辑未检查q-meat q-len只要收到首片和末片就强行拼接忽略中间片丢失导致UDP载荷中混入0填充校验和失败。解法固件升级修复重组逻辑严格校验连续性临时方案应用层分块传输每块≤1300字节预留IP首部UDP首部禁用分片setsockopt(sockfd, IPPROTO_IP, IP_MTU_DISCOVER, opt, sizeof(opt))5.3 现场三Kubernetes Pod间通信延迟突增kubectl exec响应慢现象某K8s集群Calico CNI中Pod A访问Pod B的APIP95延迟从50ms升至2000ms。诊断# 在Node上抓包 tcpdump -i cali ip[6:2] 1400 -w k8s.pcap # 发现大量Total Length1420的包且Fragment Offset均为0MF1 # 检查Calico MTU kubectl get ippool -o wide # 输出mtu: 1440 # 问题Pod内应用发送1420字节包Calico MTU1440本不应分片但包长1420 1440-201420? 等于 # 实际Calico添加BPF程序增加8字节元数据真实可用MTU143214201432但内核计算时未考虑BPF开销根因CNI插件BPF程序引入额外头部使实际MTU小于配置值内核按配置MTU判断导致本不该分片的包被错误分片。解法# Calico配置中显式设置更低MTU kubectl patch ippool default --typejson -p[{op: replace, path: /spec/metadata/annotations/projectcalico.org~1mtu, value: 1420}]5.4 现场四防火墙日志显示ICMP Fragmentation Needed但客户端未降包长现象某企业防火墙日志高频记录ICMP type 3 code 4但客户端应用无响应继续发送大包。诊断# 检查客户端是否启用PMTUD sysctl net.ipv4.ip_no_pmtu_disc # 输出1禁用PMTUD # 检查应用是否覆盖socket选项 strace -e tracesendto,sendmsg,socket,setsockopt -p $(pgrep myapp) # 发现setsockopt(3, SOL_IP, IP_MTU_DISCOVER, [0], 4) 0 # 禁用PMTUD根因应用开发者为“避免分片”而显式禁用PMTUDIP_PMTUDISC_DISABLE导致即使收到ICMP提示也不调整包长。解法应用层修复改用IP_PMTUDISC_WANT让内核自动处理网络层兜底防火墙启用TCP MSS Clamping对SYN包的MSS选项进行截断iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 13605.5 现场五Wireshark显示Malformed packet但
阅读完成 · 觉得有帮助?
咨询建站