Appearance
第7章 网络包的分层结构
在 Wireshark 中选中一条抓包记录后,Packet Details(包详情)窗格通常会列出多达几十个字段:MAC 地址、IP 地址、TTL(IPv6 中称 Hop Limit)、端口号、序列号(Seq)、确认号(Ack)、接收窗口(Window)、时间戳以及应用层数据。分析抓包的第一步,就是学会将这些字段归位到它们所属的网络层,然后顺着协议封装的顺序,在原始字节流中准确定位它们。
IP TTL 与 IPv6 Hop Limit 是什么?
IPv4 TTL 和 IPv6 Hop Limit 都是防止数据报在路由环路中无限转发的计数字段。发送端设置初值,每经过一个转发路由器通常减 1;减到 0 时,路由器丢弃数据报并可返回 ICMP Time Exceeded。
它们如今表达的是跳数上限,抓包中的剩余值可辅助推测路径变化。DNS 记录 TTL 控制缓存有效期,属于另一套独立语义。
从一个可观察的问题开始
我们先来看一个贯穿本篇的报文帧(Frame)示例。在这个例子中,客户端 192.0.2.10:49152 向服务端 198.51.100.20:9000 发送了一段文本 HELLO TCP\n:
text
0000 02 42 ac 11 00 02 02 42 ac 11 00 01 08 00 45 00
0010 00 3e 1c 46 40 00 40 06 32 22 c0 00 02 0a c6 33
0020 64 14 c0 00 23 28 6a 1b 2c 3e 10 20 30 41 80 18
0030 fa f0 e7 fd 00 00 01 01 08 0a 01 02 03 04 a0 b0
0040 c0 d0 48 45 4c 4c 4f 20 54 43 50 0a最左侧的 0000、0010 等数字表示十六进制的字节偏移量。比如 0010 代表这一行的第一个字节,是整个报文的第 16 个字节(十进制);这里每一行刚好展示 16 个字节。在往下看之前,不妨先猜一猜:TCP 首部(Header)的第一个字节在哪里?应用数据的第一个字节 48(即字符 H)又藏在哪个位置?
只要顺着网络分层逐层剖析,答案就会水落石出。
像俄罗斯套娃一样的四层封装
上面这条抓包记录,本质上是四个相互嵌套的“容器”:
text
Ethernet Frame,抓包长度 76 字节
└─ IPv4 Datagram,Total Length = 62 字节
└─ TCP Segment,首部 32 字节,数据 10 字节
└─ Application Data,"HELLO TCP\n"在不同的网络资料中,关于“包”的叫法可能比较混乱。为了避免歧义,本教程统一采用以下精确的术语定义:
| 名称 | 所属层次 | 在样本中的内容 |
|---|---|---|
| Frame(帧) | 链路层 | Ethernet 首部加上整个 IPv4 数据报 |
| Packet / Datagram(包 / 数据报) | 网络层 | IPv4 首部加上 TCP 报文段 |
| Segment(报文段) | 传输层 | TCP 首部加上 TCP Payload |
| Application Message(应用消息) | 应用层 | 示例协议定义的 HELLO TCP\n |
数据的封装发生在发送端:应用层把一串字节流交给 Socket,传输层的 TCP 协议为其加上 TCP 首部,网络层的 IP 协议再加上 IP 首部,最后数据链路层为了把包送到下一跳,再贴上链路层首部。接收端收到数据后,则按照相反的顺序一层层“拆包”。每一层只关心自己首部里的控制信息,处理完毕后,就把里面包裹的载荷(Payload)原封不动地交给上一层。
每一层回答一组问题
网络之所以要分层,核心在于“各司其职”。链路层只操心“这一跳”怎么把帧送过去,比如目标 MAC 地址和链路类型是什么;IP 层负责宏观的路由,关心数据报要送往世界上哪台主机、沿途该怎么转发,所以它看重目标 IP 地址、生存时间(TTL)以及分片信息;TCP 层作为传输层,重点关注哪两个端点在通信、哪些字节发过去了、哪些被确认了,于是就有了端口号、序列号(Seq)、确认号(Ack)和流量控制窗口(Window);最后,应用层才去解释这些剥出来的纯字节到底是个 HTTP 请求、一段文件,还是个错误响应。
需要注意的是,Payload(载荷)也是个相对概念。站在以太网(Ethernet)层的角度,整个 IPv4 数据报都是它的 Payload;站在 IPv4 层的角度,整个 TCP 报文段变成了它的 Payload;只有站在 TCP 层来看,TCP 首部后面跟着的那 10 个字节,才是真正的 TCP Payload。所以,在看 Wireshark 里的 Payload 或 Data 标签时,得先搞清楚当前看的是哪一层的“视野”。
搞懂了这种职责划分,排查网络故障就有了抓手。比如发现 TTL 变了,这是 IP 路由路径发生变化的证据;发现端口号变了,说明 TCP 端点变了或者沿途有 NAT(网络地址转换);看到应用层返回了特定的错误码,那通常是业务协议层面的问题。只要把异常现象归属到正确的层次,就能知道该用什么命令去查、该看什么日志,或者该在哪个节点抓包。
优秀的抓包分析报告,通常也会按照这个层次顺序来写:先交待网卡接口和帧级别的基础信息,接着说明 IP 层的源和目的,然后拆解 TCP 层面的四元组(源IP、源端口、目的IP、目的端口)和字节流进度,最后再结合应用层的日志得出结论。这种从下到上、逻辑严密的叙述方式,能让你的每一个排查结论都经得起推敲。
第一层:Ethernet(以太网)首部
回到前面的例子,样本帧开头的 14 个字节是这样的:
text
帧偏移 0~5 02 42 ac 11 00 02 目标 MAC
帧偏移 6~11 02 42 ac 11 00 01 源 MAC
帧偏移 12~13 08 00 EtherType = IPv4这里的 EtherType 值为 0x0800,它告诉接收方:后面的 Payload 是一个 IPv4 数据报。因此,IPv4 首部理所当然地从帧偏移 14 开始。因为 Wireshark 的十六进制偏移量是从 0 开始数的,所以 IPv4 的第一个字节 45,对应的就是十进制偏移的第 14 个字节(也就是十六进制的 0x0e 处)。
有一点很关键:MAC 地址只管当前这“一跳”的局域网链路。当路由器转发这个 IPv4 数据报时,它会毫不犹豫地剥掉并重新构造适应下一段链路的链路层首部。这就意味着,同一个 IP 包在网上传输时,沿途抓到的源 MAC 和目标 MAC 是会不断变化的。如果想深入了解路由器是怎么进行下一跳转发的,可以参考 Frame、MAC、IP 与 ARP。
在日常抓包中,普通的以太网帧通常就是这 14 字节的首部;如果是带有 VLAN 标签的网络,链路层会多出 4 个字节(802.1Q)。另外,像本地回环(Loopback)接口、PPP 拨号链路或者某些虚拟网卡,它们的链路层长得可能完全不一样。还要注意,平时用 Wireshark 存下来的抓包文件,为了省事,通常会把以太网物理层的前导码(Preamble)、帧间隙和帧尾的校验和(FCS)剥离掉。所以在分析时,务必根据当前抓包接口的实际链路类型来算长度。
第二层:IPv4 首部
上面算出来 IPv4 首部的第一个字节是 0x45。在 IP 协议里,这 8 个比特(1 字节)得拆开看:
text
0x45 = 0100 0101(二进制)
└──┘ └──┘
版本 IHL
4 5高四位的版本号是 4,代表这是 IPv4;低四位叫做 IHL(Internet Header Length,IP 首部长度),这里的值是 5。需要特别注意的是,IHL 的单位是“32 位字(即 4 字节)”。所以这个 IP 首部的实际长度是:
既然 IPv4 首部从帧偏移 14 处开始,并且自身占了 20 字节,那么下一层 TCP 首部的起点位置自然就出来了:
偏移量 34 转换成十六进制就是 0x22。我们切回开头的十六进制原始字节数据,找到 0020 这一行,往右数 2 个字节偏移,正好能看到一个 c0——这就是 TCP 首部的起点(源端口号的第一个字节)。
除了计算长度,这个样本的 IPv4 首部还透露了以下关键信息:
| IPv4 字段 | 原始字节 | 计算或结果 |
|---|---|---|
| Total Length | 00 3e | 0x003e = 62 字节 |
| TTL | 40 | 0x40 = 64 |
| Protocol | 06 | TCP |
| Header Checksum | 32 22 | 0x3222 |
| Source Address | c0 00 02 0a | 192.0.2.10 |
| Destination Address | c6 33 64 14 | 198.51.100.20 |
这里的 Total Length(总长度)是 62 字节,它是从 IPv4 第一个字节开始算的,包含了 IPv4 首部以及里面包着的 IPv4 Payload。至于最外层以太网首部的 14 字节,跟它没关系。所以在这个样本中,长度的关系是这样的:
最后,IPv4 首部里的 Protocol = 6 是一个重要的指示牌,它告诉接收方:“我肚子里的 Payload 是一个 TCP 报文段,请交给 TCP 协议栈去处理。”(在 IPv6 中,起这个指示作用的字段叫 Next Header。顺带一提,IPv6 的基本首部长度固定为 40 字节,但如果带有扩展首部,TCP 的起点位置就会相应地往后推)。
第三层:TCP 首部与 TCP Payload
刚才我们算出了 TCP 从帧偏移 34 开始。所以,本样本的 TCP 部分对应的十六进制字节是这些:
text
c0 00 23 28 6a 1b 2c 3e 10 20 30 41 80 18 fa f0
e7 fd 00 00 01 01 08 0a 01 02 03 04 a0 b0 c0 d0
48 45 4c 4c 4f 20 54 43 50 0a跟 IPv4 类似,TCP 并不是定长的,它也有个字段叫 Data Offset(数据偏移,其实就是首部长度)来标识自己的边界。在 0x80 这个字节里,它的高四位是 8,单位同样是 4 字节(32 位)。因此,这个 TCP 报文段的首部长度是:
有了 TCP 首部长度,我们就能算出最核心的应用层数据在整个报文帧里的绝对起点:
偏移量 66 写成十六进制是 0x42。目光移到 0040 那一行,往右挪 2 个位置,你会看到一个 48——恭喜,我们终于剥开层层外衣,看到了应用层数据的真面目!
其实,如果你不想去查 TCP 首部里的 Data Offset,TCP 携带的数据长度(TCP Payload Length)也是可以通过下层的长度信息倒推出来的:
把这最后 10 个字节扔给 ASCII 解码器,答案一目了然:
text
48 45 4c 4c 4f 20 54 43 50 0a
H E L L O T C P \n这 10 个字节具体表示什么业务逻辑,完全由应用层协议说了算。TCP 的职责很纯粹:“我不关心你发的是什么,我只负责把你这串数据当成有序的字节流,塞进连接通道里。”正因为 TCP 是面向字节流的协议,它不关心消息边界。一条庞大的应用层消息可能会被切碎,分装在几十个 TCP 报文段里;反之,几条短小的应用层请求,也可能被 TCP 黏合进同一个报文段里一起发走。
一张偏移量总表
| 范围,含首尾 | 长度 | 层次 | 样本内容 |
|---|---|---|---|
| 帧偏移 0~13 | 14 字节 | Ethernet | MAC、EtherType |
| 帧偏移 14~33 | 20 字节 | IPv4 | 地址、TTL、Protocol 等 |
| 帧偏移 34~65 | 32 字节 | TCP Header | 固定首部 20 字节、选项 12 字节 |
| 帧偏移 66~75 | 10 字节 | TCP Payload | HELLO TCP\n |
我们来盘一下总长度,看看对不对得上:
这个小小的加法等式非常重要,它把干瘪的原始字节、各协议层的长度控制字段,以及最终抓包文件呈现的总长度完美地串在了一起。在接下来的章节里,我们会把聚光灯打向偏移量 34~65 的这段区域,逐个扒开 TCP 首部里的那些神秘字段。
在 Wireshark 中复现实验
环境与步骤
- 打开第二篇中我们写好的客户端与服务端代码,将两端的
PORT常量从50007统一改成9000,启动双端后,随意发送一段已知内容的文本(比如HELLO TCP)。 - 打开 Wireshark,由于是本机测试,请务必监听本地回环接口(Loopback)。在 Windows/Npcap 环境下,它通常叫作
Adapter for loopback traffic capture。 - 抓包开始后,在过滤器栏输入
tcp.port == 9000 && tcp.len > 0,过滤掉无用的背景流量和空 ACK。 - 在报文列表中选中那条客户端发出的包含数据的记录,并在下方的详情窗格里,依次点开链路层(Frame/Ethernet)、Internet Protocol(IPv4)和 Transmission Control Protocol(TCP)的折叠节点。
- 试着用鼠标点击详情树里的不同字段,观察最下方 Packet Bytes(十六进制报文字节)窗格里的高亮范围是如何随着你的点击而同步移动的。
- 分别记录下 Frame Length(帧总长)、IP Header Length(IP 首部长度)、IP Total Length、TCP Header Length 以及 TCP Payload 的长度。
预期现象
- Wireshark 极具人性化地将这串二进制死数据,解析成了一棵结构清晰的协议树,依次是物理帧(Frame)、链路层、网络层(IP)、传输层(TCP)和应用层(Data)。
- 展开 IP 节点,你会发现正是
Protocol(或 IPv6 的Next Header)字段的值,指引 Wireshark 把下一层的解码器切换到了 TCP。 - 展开 TCP 节点,
Header Length(实际对应报文里的 Data Offset)会清清楚楚地告诉你 TCP 的载荷从哪里开始。 - 实验中你抓到的实际报文长度可能跟教程里不完全一样,但没关系,它们依然会完美契合上面教你的那些加减法公式。
- 如果在回环网卡上抓包,你看到的链路层可能是类似
Null/Loopback这样的虚拟首部,这很正常。Wireshark 会根据该接口的实际捕获格式,忠实地为你呈现对应的字段解析。
排查留痕指南
在日后的实际排查中,为了让别人信服你的结论,建议养成保留以下证据的习惯:
| 证据 | 记录内容 |
|---|---|
| 应用日志 | 发送字节串、发送长度、时间戳 |
| Socket 地址 | 本地端点与远端端点 |
| 抓包事实 | 帧号、接口、捕获长度、原始字节 |
| 手工推导 | 各层起点、首部长度、Payload 长度 |
| 分析器结果 | Wireshark 展示值及其字段路径 |
随堂小测
- 假设一个以太网帧使用 14 字节首部,它的 IPv4 首部的 IHL 字段值为
6,里面 TCP 首部的 Data Offset 字段值为7。请问:在这个帧中,TCP 报文段的起点偏移量是多少?应用层数据的起点偏移量又是多少? - 某 IPv4 数据报的 Total Length 为 100 字节,其中 IPv4 首部占了 24 字节,TCP 首部占了 28 字节。请算出它的 TCP Payload(载荷)到底有多少字节?
- 我们在服务端网卡抓到的包中,目标 MAC 地址,往往跟客户端网卡发包时的目标 MAC 地址不一样。这种现象是因为哪一层的协议存在“逐跳转发(Hop-by-Hop)”特性?
参考答案
- IP 首部的实际长度为
字节。因此,TCP 的起点等于以太网首部长度加上 IP 首部长度: ;另外 TCP 首部的实际长度为 字节,所以应用层数据的绝对起点偏移为: 。 字节。- 链路层。因为当 IP 数据报跨越不同网段时,路由器在转发过程中,会把旧的链路层首部剥掉,并针对下一段具体的链路重新构造一个新的以太网(或其他协议)首部。
本章小结
- 网络协议的封装就像俄罗斯套娃,以太网帧(Frame)、IP 数据报(Datagram)、TCP 报文段(Segment)和应用层消息构成了一个逐层嵌套的关系。
- 找准数据边界的密码,藏在上一层的协议指示(比如
Protocol字段)和本层的首部长度(比如IHL和Data Offset)里,它们共同决定了下一层协议的解析起点。 - 记住日常抓包中最经典的数值:普通的 TCP 首部往往从帧偏移量 34 开始,而应用层真正的业务数据常常从帧偏移量 54 开始(如果 TCP 没有可选字段)。在咱们这个带着 12 字节 TCP Option 的例子里,应用数据从偏移量 66 处开始。
- 会算各种长度不仅是为了排查问题留证,它更是我们后续深入理解和推算 TCP 序列号(Seq)和确认号(Ack)行为的基石。
- 你选的网卡类型(比如以太网网卡或回环网卡)决定了链路层长什么样。在分析时,不要死记硬背固定偏移,一切都要从当前实际捕获到的帧格式出发。