IPv4报文结构:拆开看看数据包长什么样

说实话,IPv4报文结构这东西,刚入行时我觉得就是背背字段长度、记记偏移量。直到有一次在现网抓包排查一个诡异的丢包问题,折腾了三天才发现是首部长度字段被篡改了。从那以后,我再也不敢小看这20字节的固定首部。

今天咱们就把IPv4报文从头到尾拆开,每个字段我都会结合实战经验来讲。你想想看,一个数据包在网络上跑,靠的就是这些字段在“指路”。

核心要点:IPv4首部固定20字节,选项字段可变,总长度不超过65535字节。记住这个范围,很多排障思路都从这里出发。

2.1 版本与首部长度——最前面的两个“身份证”

报文最开始的两个字段,各占4位(半个字节)。

  • 版本(Version):固定为4,表示IPv4。如果是IPv6,这里就是6。我在抓包时经常先看这个字段,确认设备确实在跑IPv4协议栈。
  • 首部长度(IHL):单位是4字节。最小值5(20字节),最大值15(60字节)。为什么是5?因为固定首部就是20字节,20/4=5。

实战技巧:我曾经遇到过一个防火墙配置错误,把首部长度字段改成了非5的值,导致后续路由器解析出错。排查时用Wireshark一看,IHL字段显示为7,明显异常。所以抓包时养成习惯,先瞄一眼这个字段对不对。

2.2 服务类型(ToS)——你的数据包“急不急”

这个字段8位,早期设计是用来做QoS的。说白了,就是告诉路由器:我这个包是“加急件”还是“平邮”。

实际项目中,ToS字段用得并不多。但如果你在做VoIP或视频会议,就需要关注DSCP(差分服务代码点)了,它占ToS字段的前6位。

优先级 DSCP值 典型应用
EF(加速转发) 46 (0x2E) VoIP语音
AF41 34 (0x22) 视频会议
AF21 18 (0x12) 关键业务数据
BE(尽力而为) 0 (0x00) 普通上网

注意:别以为设置了高优先级就万事大吉。端到端路径上的所有设备都必须支持并信任这个标记,否则你的“加急件”照样被当平邮处理。我曾经在跨运营商链路上吃过这个亏。

2.3 总长度——这个包到底有多大?

16位字段,单位是字节。最大值65535,包含首部和数据。

这里有个坑:总长度字段的值必须大于首部长度字段的值(因为至少得有数据吧)。如果总长度小于20字节,那这个包就是非法的,路由器会直接丢弃。

我记得有一次排查MTU问题,发现某个中间设备把总长度字段改小了,导致接收端校验失败。嗯,这种问题抓包一看就现原形。

2.4 标识、标志、片偏移——分片三兄弟

这三个字段放在一起讲,因为它们共同完成IP分片和重组的工作。

  • 标识(Identification):16位,发送端每发一个包就加1。分片时,同一个原始包的所有分片共享同一个标识值。
  • 标志(Flags):3位,只有后两位有用。第2位DF(Don't Fragment),设为1表示不允许分片;第3位MF(More Fragments),设为1表示后面还有分片。
  • 片偏移(Fragment Offset):13位,单位是8字节。表示这个分片在原始包中的位置。

分片规则速记:每个分片的数据长度必须是8的倍数(除了最后一个分片)。为什么?因为片偏移的单位是8字节,不够8字节没法对齐。

我遇到过最典型的场景:VPN隧道封装后,原始包加上新头部超过了链路MTU,DF位又被设置了,结果包直接丢了。那时候排查了很久,最后在路由器上抓包看到ICMP“需要分片但DF已设置”的错误消息才恍然大悟。

2.5 生存时间(TTL)——防止数据包“鬼打墙”

8位字段,每经过一个路由器减1。减到0时,路由器丢弃该包,并发送ICMP超时消息给源端。

这个字段太实用了。我经常用它来诊断网络路径:

# Linux/Mac上使用traceroute
traceroute -n 8.8.8.8

# Windows上使用tracert
tracert -d 8.8.8.8

原理很简单:traceroute先发一个TTL=1的包,第一个路由器返回超时;再发TTL=2,第二个路由器返回……直到到达目标。这样就能画出完整的路径图。

避坑指南:我曾经发现某个Linux服务器默认TTL是64,但Windows是128。如果跨系统做traceroute,看到跳数不一致别慌,先看看初始TTL值。

2.6 协议字段——数据交给谁?

8位字段,标识上层协议。常见的值:

协议号 协议名称 说明
1 ICMP 网络控制消息,ping就用它
6 TCP 可靠传输,网页、邮件都用它
17 UDP 不可靠但快,DNS、视频流用它
89 OSPF 动态路由协议

这个字段在配置ACL(访问控制列表)时特别重要。比如你想允许ping但禁止网页访问,就得分别匹配协议号1和6。

2.7 首部校验和——数据完整性“守门员”

16位字段,只校验首部,不校验数据。为什么?因为上层协议(TCP/UDP)自己会做校验和,IP层没必要重复劳动。

计算方法是:把首部看成16位整数的序列,求和后取反码。路由器每经过一跳,TTL变了,校验和就得重新算。

注意:IPv6已经去掉了首部校验和字段,因为链路层(以太网)和上层协议已经提供了足够的校验。但IPv4里这个字段还是必须的,别搞混了。

2.8 源IP地址与目的IP地址——数据包的“发件人”和“收件人”

各32位,用点分十进制表示。比如192.168.1.1。

这两个字段是整个IP协议的核心。源地址告诉别人“我从哪来”,目的地址告诉网络“我要去哪”。

我在做NAT(网络地址转换)配置时,经常要盯着这两个字段看。内网地址怎么映射成公网地址,源地址和目的地址怎么替换,搞错了整个网络就瘫了。

2.9 一张图看懂IPv4报文结构

下面这张SVG图把整个IPv4首部结构画出来了,每个字段的位置和长度一目了然。

IPv4报文首部结构(20字节固定部分) 版本 4位 首部长度 4位 服务类型(ToS) 8位 总长度 16位 标识 16位 标志 3位 片偏移 13位 生存时间(TTL) 8位 协议 8位 首部校验和 16位 源IP地址 32位 目的IP地址 32位 选项字段(可选,0-40字节,使首部长度超过20字节) 字段长度速记 • 版本/首部长度:各4位(半个字节) • ToS/协议/TTL:各8位(1字节) • 总长度/标识/校验和:各16位(2字节) • 源/目的IP地址:各32位(4字节) 分片三字段关系 • 标识:同一原始包的分片值相同 • 标志:DF=1禁止分片,MF=1还有后续 • 片偏移:单位8字节,指示分片位置 常见协议号 • 1:ICMP(ping/traceroute) • 6:TCP(HTTP/SSH/邮件) • 17:UDP(DNS/DHCP/视频) • 89:OSPF(动态路由)

2.10 实战:用Python解析IPv4首部

光说不练假把式。下面这段代码演示了如何从原始字节中解析IPv4首部。我在做网络监控工具时就用过类似的逻辑。

import struct

def parse_ip_header(raw_bytes):
    # 前20字节是固定首部
    header = raw_bytes[:20]
    
    # 使用struct解包:!表示网络字节序,BBHHHBBH4s4s
    # B=1字节, H=2字节, 4s=4字节字符串
    ver_ihl, tos, total_len, ident, flags_frag, ttl, proto, checksum, src, dst = \
        struct.unpack('!BBHHHBBH4s4s', header)
    
    version = ver_ihl >> 4
    ihl = ver_ihl & 0x0F
    
    print(f"版本: IPv{version}")
    print(f"首部长度: {ihl * 4} 字节")
    print(f"总长度: {total_len} 字节")
    print(f"标识: {ident}")
    print(f"TTL: {ttl}")
    print(f"协议: {proto}")
    print(f"源IP: {'.'.join(map(str, src))}")
    print(f"目的IP: {'.'.join(map(str, dst))}")
    
    return version, ihl, total_len

# 示例用法(假设从socket收到原始数据包)
# parse_ip_header(raw_packet)

小提示:实际抓包时,Wireshark已经帮你解析好了所有字段。但自己写代码解析一遍,对理解报文结构帮助很大。我建议每个网络工程师都至少手写一次这个解析函数。

好了,IPv4报文结构就讲到这里。每个字段都有它的设计目的,也都有对应的排障场景。下次你再遇到网络问题,不妨先抓个包,对照着这12个字段逐个检查,很多问题就能迎刃而解。