MINIBLOG

Blog Note Tags Links About
Home Search
Mar 3, 2026
miniyuan

计算机网络 Lab1 协议分析


一、环境配置

如果想要直接使用 root 登录虚拟机(当然你也可以 su -),可以修改 /etc/ssh/sshd_config 中该行:

# 禁止远程使用密码登录 root
# PermitRootLogin prohibit-password
# 允许远程使用密码登录 root
PermitRootLogin yes

并把宿主机 ssh 配置修改为 root 用户即可。以下所有命令均在 su 权限下执行。

二、Scapy tutorial

Scapy 是一个用 Python 编写的、能够操作数据包的强大程序。它既可以作为交互式工具使用,也可以作为库导入到你的 Python 代码中。它让你能够发送(send)、嗅探(sniff)、解析(parse)和伪造(forge)网络数据包。

lab 中提供了一个教学文件 tutorial.ipynb,可用以学习 scapy

1. 打开方式

最好选用 jupyter 打开,避免 scapy 运行时可能遇到的 root 权限获取问题。运行

jupyter lab --allow-root --no-browser --ip=127.0.0.1 --port=10000 --NotebookApp.token='' --NotebookApp.password=''

打开一个 10000 端口上的服务器进程 lab,--ip=0.0.0.0 表示监听所有网络接口。 然后在浏览器中访问 http://127.0.0.1:10000/lab 即可进行交互。实际上不需要监听所有接口,只需要监听 --ip=127.0.0.1 即可。 对于这一点,可以见后面的补充内容

2. 运行

注意嗅探报文时 wget http://baidu.com 应当在虚拟机进程中运行。

另外,如果无法执行 p1.pdfdump('pkt.pdf')(显示缺少依赖),可以直接在 jupyter 中这一行之前先运行

pip install pyx

将 pyx 装入 ipy 内核。根本原因是 ipy 的 python 路径可能和终端 python 路径不太一致,导致你在终端安装的 pyx 不一定能被 ipy 找到。

3. 补充内容

运行 jupyter 会涉及三个进程:

  • 服务器进程(虚拟机):作为 jupyter 的服务器进程,在指令启动的终端中运行。
  • 内核进程(虚拟机):此内核非彼内核。这是在我们打开 notebook 时自动启动的,是执行 python 代码(如 sniff)的地方。
  • 客户端进程(宿主机):在宿主机上通过 http://127.0.0.1:10000 打开的浏览器进程,显示代码和结果。

正常运行时,通过宿主机的客户端进程,向 127.0.0.1 也就是宿主机自己的 10000 端口发送请求,VirtualBox 的端口转发(NAT 模式)捕获到这一请求,将请求转发给虚拟机的 10000 端口。 而虚拟机的 10000 端口是服务器进程正在监听的,它在创建时就已经和内核进程通过 ZeroMQ 机制进行了连接,此时服务器进程和内核进程通信并拿到运算结果,然后再将数据原路送回。

[客户端进程] (宿主机)
    ├── 浏览器访问: http://127.0.0.1:10000
    └── 请求从这里发出

        ↓ HTTP协议 (TCP)

[VirtualBox NAT] (宿主机内核)
    ├── 配置: LocalhostReachable=1 (默认)
    ├── 对于特殊的 127.0.0.1,检查宿主机本地
    │   ├── 如果 10000 有服务: 不转发给虚拟机
    │   └── 如果 10000 无服务: 转发给虚拟机
    ├── 查找端口转发规则: 主机10000 → 虚拟机10000
    ├── 执行特殊的地址转换:
    │   ├── 目标地址: 127.0.0.1:10000 → 保持 127.0.0.1:10000
    │   └── 源地址: 宿主机IP → 10.0.2.2 (NAT网关)
    └── 通过特殊通道转发到虚拟机

        ↓ 转发到虚拟机

[服务器进程] (虚拟机)
    ├── 监听地址: 由 --ip 决定
    │   ├── 如果 --ip=127.0.0.1: 只监听 127.0.0.1,成功
    │   └── 如果 --ip=0.0.0.0: 监听所有地址
    ├── 通过 ZeroMQ 与内核进程通信
    └── 将执行结果返回给客户端

        ↓ ZeroMQ协议 (TCP)

[内核进程] (虚拟机)
    ├── 主动连接服务器进程的 ZeroMQ 端口,打开notebook时自动启动
    ├── 目标地址: 服务器绑定的ZeroMQ地址
    └── 执行代码: sniff() 在这里运行

二、常见网络协议

第一层(物理层)

┌───────────────┬───────────────┬─────────────────────────────────────┐
│   Preamble    │      SFD      │              MAC Frame              │
│     (7B)      │     (1B)      │  (Dest MAC + Src MAC + Type + ...)  │
└───────────────┴───────────────┴─────────────────────────────────────┘

其中:

  • Preamble 前导码/前同步码:交替的 10,用于进行发送端与接收端的时钟同步
  • Start Frame Delimiter 帧首定界符:10101011 表示帧要开始了

第二层(数据链路层):Ether 协议

以太网是目前最普及的 LAN 技术。按照以太网协议,其帧(Ethernet Frame)结构如下:

┌───────────────┬───────────────┬───────────────┬───────────────────┬──────────────┐
│ Dest MAC (6B) │ Src MAC (6B)  │ EtherType(2B) │ payload(46-1500B) │   FCS (4B)   │
└───────────────┴───────────────┴───────────────┴───────────────────┴──────────────┘

其中:

  • MAC 地址:48位(6字节),如 00:11:22:33:44:55。每个网卡拥有全球唯一的 MAC 地址。
  • EtherType 类型字段:指示上层协议,常见值:
    • 0x0800:IPv4
    • 0x0806:ARP
    • 0x86DD:IPv6
  • payload:由于帧最少 64B,故 payload 最少 46B,不足的会添加 padding
  • FCS(帧校验序列):用于检测传输错误,但 Scapy 通常不捕获此字段。
  • MTU(Maximum Transmission Unit):最大传输单元,此处是 1500B

第三层(网络层):IPv4 协议

IP协议负责将数据包(packet/IP分组)从源主机路由到目的主机。其第 4 版 IPv4 协议规定头部结构如下1:

0B              1B              2B              3B              4B
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Ver  |  IHL  |Type of Service|         Total Length          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|        Identification         |Flags|     Fragment Offset     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Time to Live |   Protocol    |      Header Checksum          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       Source Address                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                     Destination Address                       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      Options (Optional)                       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
字段位数说明
Ver (Version)4位版本号。例如 IPv4 取值为 4
IHL (Internet Header Length)4位互联网首部长度。实际就是 IPv4 首部长度,以 32 位(4B)为单位。最小值为 5(20B,也即无 Options),最大值为 15(60B)
Type of Service8位服务类型。用于 QoS(服务质量)和包优先级,现常被 DSCP(区分服务代码点)替代
Total Length16位总长度。整个 IP 包的大小(首部 + 数据),以字节为单位。最大值为 65535 字节
Identification16位标识符。用于分片与重组,同一原始数据包的所有分片共享相同标识符
Flags3位标志位。控制分片行为:
第 0 位:保留,必须为 0
第 1 位:DF(Don’t Fragment,禁止分片)
第 2 位:MF(More Fragments,后面还有分片)
Fragment Offset13位片偏移。当前分片在原始数据包中的位置,以 8 字节为单位,13 + 3 = 16
Time to Live (TTL)8位生存时间。每经过一个路由器减 1,减到 0 时丢弃,防止无限循环
Protocol8位协议号。标识上层协议:
6:TCP
17:UDP
1:ICMP
等等
Header Checksum16位首部校验和。仅对首部进行错误检测,每经过一个路由器都会重新计算
Source Address32位源 IP 地址。发送方的 IPv4 地址
Destination Address32位目的 IP 地址。接收方的 IPv4 地址
Options可变(0-40 字节)选项字段。可选功能(如时间戳、路由记录等),很少使用。如有选项,需用填充位保证首部是 32 位的整数倍

注:

  • 固定首部长度:不含 Options 时为 20 字节
  • 最大首部长度:含 Options 时可达 60 字节
  • 整个包最大长度:65535 字节(但链路层通常限制为 1500 字节,超过需分片)

第四层(传输层):TCP 协议

TCP 协议负责提供可靠的、面向连接的数据传输服务,用以传输TCP段(TCP Segment)。其段结构如下:

0B              1B              2B              3B              4B
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          Source Port          |        Destination Port       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  Sequence Number (SEQ)                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|               Acknowledgment Number (ACK)                     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Offset | Rsrvd |     Flags     |          Window Size          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|            Checksum           |         Urgent Pointer        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
字段位数说明
Source Port16位源端口号。发送方的端口号,用于标识发送应用程序
Destination Port16位目的端口号。接收方的端口号,用于标识接收应用程序
Sequence Number (SEQ)32位序列号。用于数据流排序:
如果是 SYN 包,表示初始序列号(ISN)
如果是数据包,表示本段数据第一个字节的序列号
Acknowledgment Number (ACK)32位确认号。只有当 ACK 标志位为 1 时才有效,表示期望收到的下一个字节的序列号
Offset4位数据偏移。实际就是 TCP 首部长度,以 32 位(4B)为单位。最小值为 5(20B),最大值为 15(60B)
Rsrvd (Reserved)4位保留位。预留将来使用,必须为 0
Flags8位标志位。每个标志位占 1 位,用于控制连接状态:
CWR (Congestion Window Reduced):拥塞窗口减少
ECE (ECN-Echo):ECN 回显
URG (Urgent):紧急指针字段有效
ACK (Acknowledgment):确认号字段有效
PSH (Push):推送数据,接收方应尽快交给应用层
RST (Reset):重置连接
SYN (Synchronize):同步序列号,用于建立连接
FIN (Finish):发送方已完成数据发送
Window Size16位窗口大小。用于流量控制,表示发送方愿意接收的字节数(从确认号开始)
Checksum16位校验和。对整个 TCP 段(首部 + 数据)进行错误检测,还包括伪首部(含源 IP、目的 IP 等)
Urgent Pointer16位紧急指针。只有当 URG 标志位为 1 时才有效,表示紧急数据在段中的结束位置

注:

  • TCP 固定首部长度:不含选项时为 20 字节
  • TCP 最大首部长度:含选项时可达 60 字节
  • 伪首部(Pseudo Header): 校验和计算时,需要 12 字节信息(源 IP、目的 IP、协议号、TCP 长度)。 否则校验和中不包含 IP 信息,也即接收方的传输层无法利用其确定 IP 分组是否是正确传给自己的。 但是为了职责分离,传输层不应直接操作 IP,故而向网络层索要服务,网络层给予它一个伪首部用于临时计算,并不传输。
0B              1B              2B              3B              4B
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       Source IP Address                       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                     Destination IP Address                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Padding Zero  |   Protocol    |          TCP Length           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • 标志位位于 TCP 头部的第 14 个字节,共 8 位:
标志名称含义
SYN同步标志请求建立连接
ACK确认标志确认号有效
FIN结束标志请求断开连接
RST重置标志连接异常中断
PSH推送标志接收方应尽快交给应用层
URG紧急标志紧急指针有效

Scapy中标志位的表示:

  • 数值表示:SYN=0x02, ACK=0x10, SYN+ACK=0x12
  • 字符串表示:'S'(SYN), 'A'(ACK), 'SA'(SYN+ACK)

TCP 三次握手

TCP建立连接需要三次握手。

【第一次握手】客户端 → 服务器
    包类型:SYN
    标志位:SYN=1, ACK=0
    序列号:Seq=x
    确认号:Ack=0(无效)
    含义:客户端请求建立连接,初始序列号为x
    状态:客户端进入 SYN_SENT 状态

【第二次握手】服务器 → 客户端
    包类型:SYN-ACK
    标志位:SYN=1, ACK=1
    序列号:Seq=y
    确认号:Ack=x+1
    含义:服务器同意建立连接,确认收到客户端的SYN
    状态:服务器进入 SYN_RCVD 状态

【第三次握手】客户端 → 服务器
    包类型:ACK
    标志位:SYN=0, ACK=1
    序列号:Seq=x+1
    确认号:Ack=y+1
    含义:客户端确认收到服务器的SYN+ACK
    状态:双方进入 ESTABLISHED 状态,连接建立成功

【HTTP请求】客户端 → 服务器
    标志位:通常为 ACK+PUSH (PA)
    数据:包含HTTP请求(GET /index.html)

【HTTP响应】服务器 → 客户端
    标志位:通常为 ACK (A) 或 ACK+PUSH (PA)
    数据:包含HTTP响应(200 OK)
    确认号:对HTTP请求数据的确认

TCP 四元组与流

一个TCP流由以下四个元素唯一标识,称为四元组:

(源IP地址, 目的IP地址, 源端口, 目的端口)

注:通信是双向的,同一个流有两个方向的包。以下就是同一个流。

客户端 → 服务器: (192.168.1.2, 192.168.1.1, 54321, 80)
服务器 → 客户端: (192.168.1.1, 192.168.1.2, 80, 54321)

三、Scapy 中的 Packet 数据结构

在Scapy中,一个网络包是由多个协议层叠加而成的:

from scapy.all import *

# 创建一个HTTP请求包(从以太网到应用层)
packet = Ether()/IP(dst="192.168.1.1")/TCP(dport=80)/Raw(load="GET / HTTP/1.1\r\n\r\n")

# 层次结构:
# └── Ether           (数据链路层)
#     └── IP          (网络层)
#         └── TCP     (传输层)
#             └── Raw (应用层数据)

Packet的核心属性

属性名类型说明示例
.namestr协议名称'Ethernet', 'IP', 'TCP'
.timefloat包的时间戳1645234567.123456
.lenint包的总长度1518
.payloadPacket下一层协议数据2pkt.payload
.underlayerPacket上一层协议数据pkt.underlayer
.srcstr源地址3因层而异
.dststr目的地址因层而异

Packet的核心方法

1. 查看包信息

# 1. 带格式的详细输出
pkt.show()
"""
###[ Ethernet ]### 
  dst= ff:ff:ff:ff:ff:ff
  src= 00:11:22:33:44:55
  type= 0x800
###[ IP ]### 
  src= 192.168.1.1
  dst= 192.168.1.2
###[ TCP ]### 
  sport= 20
  dport= 80
  flags= S
"""
# 2. 一行摘要
pkt.summary()  
# 输出: 'Ether / IP / TCP 192.168.1.1:20 > 192.168.1.2:80 S'
# 3. 十六进制查看
pkt.hexdump()
# 输出原始字节的十六进制和ASCII表示

2. 检查协议层

# 方法1:haslayer()
pkt.haslayer(IP)    # True - 检查是否包含IP层
pkt.haslayer(TCP)   # True
pkt.haslayer(UDP)   # False

# 方法2:in操作符
IP in pkt           # True
TCP in pkt          # True

3. 获取特定协议层

# 方法1:getlayer()
pkt.getlayer(IP)    # 返回IP层对象

# 方法2:索引语法
pkt[IP]             # 获取IP层
pkt[TCP]            # 获取TCP层
pkt[Ether]          # 获取以太网层

# 按层索引获取
pkt[0]              # 第一层(通常是Ether)
pkt[1]              # 第二层(通常是IP)
pkt[2]              # 第三层(通常是TCP)

4. 获取多层信息

# 获取所有层
layers = pkt.layers()  # 返回层类型列表

# 获取最后一层
last = pkt.lastlayer()

# 遍历所有层
for layer in pkt:
    print(layer.name)

5. 复制和修改

# 深拷贝
pkt2 = pkt.copy()

# 修改字段
pkt2[IP].src = "10.0.0.1"  # 修改源IP

各层核心协议字段

可以对照前面的常见网络协议进行学习。

Ether层(数据链路层)

# 前提:Ether in pkt
pkt[Ether].src      # 源MAC地址 (字符串)
pkt[Ether].dst      # 目的MAC地址 (字符串)
pkt[Ether].type     # 上层协议类型 (整数)
# 常用type: 0x0800=IP, 0x0806=ARP

IP层(网络层)

# 前提:IP in pkt
pkt[IP].src         # 源IP地址 (字符串)
pkt[IP].dst         # 目的IP地址 (字符串)
pkt[IP].proto       # 上层协议 (整数)
# 常用proto: 6=TCP, 17=UDP, 1=ICMP

pkt[IP].len         # IP总长度 (首部+数据)
pkt[IP].ihl         # IP首部长度 (4字节为单位)
pkt[IP].ttl         # 生存时间

TCP层(传输层)

# 前提:TCP in pkt
pkt[TCP].sport      # 源端口 (整数)
pkt[TCP].dport      # 目的端口 (整数)
pkt[TCP].seq        # 序列号 (整数)
pkt[TCP].ack        # 确认号 (整数)
pkt[TCP].flags      # 标志位

# flags标志位的多种检查方式
pkt[TCP].flags == 0x02        # 只包含SYN
pkt[TCP].flags == 'S'         # Scapy的符号表示
'S' in pkt[TCP].flags         # 检查是否有SYN标志

# 常用flags值:
# 0x02 = 'S'  (SYN)
# 0x10 = 'A'  (ACK)
# 0x12 = 'SA' (SYN+ACK)
# 0x11 = 'FA' (FIN+ACK)

UDP层(传输层)

# 前提:UDP in pkt
pkt[UDP].sport      # 源端口
pkt[UDP].dport      # 目的端口
pkt[UDP].len        # UDP长度

Raw层(应用层数据)

# 前提:Raw in pkt
pkt[Raw].load       # 原始载荷数据 (bytes)
# HTTP请求内容(如"GET / HTTP/1.1")就在这里

常用函数

1. 嗅探函数4

# 抓取5秒的包
pkts = sniff(timeout=5)           # 抓5秒

# 抓取指定数量的包
pkts = sniff(count=10)             # 抓10个包

# 带过滤条件的抓包
pkts = sniff(filter="tcp port 80", timeout=5)  # 只抓HTTP流量

过滤语法示例:

  • "tcp":只抓TCP包
  • "port 80":端口为80
  • "host 192.168.1.1":主机为192.168.1.1
  • "tcp and port 80":TCP且端口80

2. 读写文件

# 读取pcap文件
pkts = rdpcap("http.cap")

# 写入pcap文件
wrpcap("output.cap", pkts)

# 查看文件中的包数量
count = len(pkts)

3. 包列表操作

# 索引访问
first_pkt = pkts[0]      # 第一个包
second_pkt = pkts[1]     
last_pkt = pkts[-1]       # 最后一个包

# 切片
subset = pkts[5:10]       # 第6-10个包

# 遍历
for pkt in pkts:
    # 处理每个包
    pass

4. 过滤包

# 方法1:列表推导式
http_pkts = [p for p in pkts if TCP in p and p[TCP].dport == 80]

# 方法2:filter函数
http_pkts = list(filter(lambda p: TCP in p and p[TCP].dport == 80, pkts))

四、实验内容

Q1

持续抓包 5 秒,保存为 http.cap

Trace1 = "http.pcap"

pkts = sniff(timeout=5)
wrpcap(filename=Trace1, pkt=pkts)

Q2

找到第一个目的端口为 80 的 TCP 分组,返回源 MAC 和目的 MAC。但是题目中注明

这个分组是第一个HTTP请求分组

我的理解是这里寻找的不是三次握手期间的 TCP 分组,而是请求建立完成后的 HTTP 请求分组,故我检查了 Raw。不知道对不对 www

pkts = rdpcap(filename=Trace1) # 读入
    for pkt in pkts: # 遍历
        if TCP in pkt and pkt[TCP].dport == 80 and Raw in pkt:
            # 包含 TCP 协议
            # 目的端口为 80
            # 且包含 http 请求(即有 Raw)
            src_mac = pkt[Ether].src
            dst_mac = pkt[Ether].dst
            break

Q3

返回 Q2 分组的应答分组的二层 type、三层 proto、时间戳

提供的代码缺少了 timeStamp 可以自己补上。在我上一问理解的基础上,应该就是寻找 HTTP 相应分组。

req_pkt = None
idx = None

pkts = rdpcap(filename=Trace1) # 读取
for i, pkt in enumerate(pkts): # 查找上一问中的包
    if TCP in pkt and pkt[TCP].dport == 80 and Raw in pkt:
        req_pkt = pkt
        idx = i
        break

# 注意,必须从找到的包之后开始找,否则会找错!
for pkt in pkts[idx + 1:]:
    if (TCP in pkt and 
    pkt[TCP].sport == req_pkt[TCP].dport and
    pkt[TCP].flags & 0x10):
        theType = pkt[Ether].type
        theProto = pkt[IP].proto
        timeStamp = pkt.time
        break

Q4

返回记录中分组数量。

pkts = rdpcap(filename=Trace2)
theLength = len(pkts)

Q5

返回IP分组、TCP分组、UDP分组的数量。

pkts = rdpcap(filename=Trace2)
    for pkt in pkts:
        if TCP in pkt:
            num_tcp += 1
        if UDP in pkt:
            num_udp += 1
        if IP in pkt:
            num_ip += 1

Q6

返回TCP流的数量,注意四元组相同的算一个流。

pkts = rdpcap(filename=Trace2)
    for pkt in pkts:
        if TCP in pkt:
            src_ip = pkt[IP].src
            dst_ip = pkt[IP].dst
            src_port = pkt[TCP].sport
            dst_port = pkt[TCP].dport
            if ((src_ip, dst_ip, src_port, dst_port) in flows 
            or (dst_ip, src_ip, dst_port, src_port) in flows):
                continue
            flows.add((src_ip, dst_ip, src_port, dst_port))

Q7

返回IP分组长度的最小值、中位数、最大值。

不太能理解题目希望的长度计算是个什么逻辑。 但是直接使用 len(raw(pkt)) 是不行的,因为它计算的是包的真实长度,而 pkt[IP].len 则是利用 IPv4 协议中的 Total Length 字段(见IPv4 协议)计算的长度。 也就是说,测试数据中有一些包的真实长度和 IPv4 协议中的 Total Length 字段是不匹配的! 具体差别可通过以下代码查看:

def packet_check(pkt):
    if IP in pkt:
        ip_len = len(pkt[IP].payload) + (pkt[IP].ihl * 4)
        if pkt[IP].len != ip_len:
            print(f"pkt[IP].len = ({pkt[IP].len})")
            print(f"ip_len = ({ip_len})")
            print(f"len(raw(pkt)) = ({len(raw(pkt))})")

不过似乎直接 pkt[IP].len + 18 也行?另外中位数不知道要不要取整 www

pkts = rdpcap(filename=Trace2)
pkt_lens = []

for pkt in pkts:
    if IP in pkt and Ether in pkt:
        # 以太网头部
        ether_head_len = 14
        # Dot1Q 头部
        dot1q_len = 4 if Dot1Q in pkt else 0
        # IP 分组
        ip_len = pkt[IP].len
        # padding 不计算
        # ether_tail_len = len(pkt[Padding].load) if Padding in pkt else 0

        pkt_lens.append(ether_head_len + dot1q_len + ip_len)

pkt_lens.sort()
ip_tot = len(pkt_lens)

min_length = pkt_lens[0]
max_length = pkt_lens[-1]
if ip_tot % 2 == 1:
    median_length = pkt_lens[ip_tot // 2]
else:
    # 不知道要不要取整 www
    median_length = (pkt_lens[ip_tot // 2 - 1] + pkt_lens[ip_tot // 2]) / 2

Footnotes

  1. 上述数字标注在对应比特的中部 ↩

  2. 协议数据指 head + payload ↩

  3. 包含 MAC 地址、IP 地址、端口号 ↩

  4. 这里的包实际上是指数据链路层的 Frame ↩

目录
  • 一、环境配置
  • 二、Scapy tutorial
    • 1. 打开方式
    • 2. 运行
    • 3. 补充内容
  • 二、常见网络协议
    • 第一层(物理层)
    • 第二层(数据链路层):Ether 协议
    • 第三层(网络层):IPv4 协议
    • 第四层(传输层):TCP 协议
    • TCP 三次握手
    • TCP 四元组与流
  • 三、Scapy 中的 Packet 数据结构
    • Packet的核心属性
    • Packet的核心方法
      • 1. 查看包信息
      • 2. 检查协议层
      • 3. 获取特定协议层
      • 4. 获取多层信息
      • 5. 复制和修改
    • 各层核心协议字段
      • Ether层(数据链路层)
      • IP层(网络层)
      • TCP层(传输层)
      • UDP层(传输层)
      • Raw层(应用层数据)
    • 常用函数
      • 1. 嗅探函数4
      • 2. 读写文件
      • 3. 包列表操作
      • 4. 过滤包
  • 四、实验内容
    • Q1
    • Q2
    • Q3
    • Q4
    • Q5
    • Q6
    • Q7
  • Footnotes
© 2026 miniyuan. All rights reserved.
Go to miniyuan's GitHub repo