计算机网络 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:IPv40x0806:ARP0x86DD: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 Service | 8位 | 服务类型。用于 QoS(服务质量)和包优先级,现常被 DSCP(区分服务代码点)替代 |
| Total Length | 16位 | 总长度。整个 IP 包的大小(首部 + 数据),以字节为单位。最大值为 65535 字节 |
| Identification | 16位 | 标识符。用于分片与重组,同一原始数据包的所有分片共享相同标识符 |
| Flags | 3位 | 标志位。控制分片行为: 第 0 位:保留,必须为 0 第 1 位:DF(Don’t Fragment,禁止分片) 第 2 位:MF(More Fragments,后面还有分片) |
| Fragment Offset | 13位 | 片偏移。当前分片在原始数据包中的位置,以 8 字节为单位,13 + 3 = 16 |
| Time to Live (TTL) | 8位 | 生存时间。每经过一个路由器减 1,减到 0 时丢弃,防止无限循环 |
| Protocol | 8位 | 协议号。标识上层协议: 6:TCP 17:UDP 1:ICMP 等等 |
| Header Checksum | 16位 | 首部校验和。仅对首部进行错误检测,每经过一个路由器都会重新计算 |
| Source Address | 32位 | 源 IP 地址。发送方的 IPv4 地址 |
| Destination Address | 32位 | 目的 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 Port | 16位 | 源端口号。发送方的端口号,用于标识发送应用程序 |
| Destination Port | 16位 | 目的端口号。接收方的端口号,用于标识接收应用程序 |
| Sequence Number (SEQ) | 32位 | 序列号。用于数据流排序: 如果是 SYN 包,表示初始序列号(ISN) 如果是数据包,表示本段数据第一个字节的序列号 |
| Acknowledgment Number (ACK) | 32位 | 确认号。只有当 ACK 标志位为 1 时才有效,表示期望收到的下一个字节的序列号 |
| Offset | 4位 | 数据偏移。实际就是 TCP 首部长度,以 32 位(4B)为单位。最小值为 5(20B),最大值为 15(60B) |
| Rsrvd (Reserved) | 4位 | 保留位。预留将来使用,必须为 0 |
| Flags | 8位 | 标志位。每个标志位占 1 位,用于控制连接状态: CWR (Congestion Window Reduced):拥塞窗口减少 ECE (ECN-Echo):ECN 回显 URG (Urgent):紧急指针字段有效 ACK (Acknowledgment):确认号字段有效 PSH (Push):推送数据,接收方应尽快交给应用层 RST (Reset):重置连接 SYN (Synchronize):同步序列号,用于建立连接 FIN (Finish):发送方已完成数据发送 |
| Window Size | 16位 | 窗口大小。用于流量控制,表示发送方愿意接收的字节数(从确认号开始) |
| Checksum | 16位 | 校验和。对整个 TCP 段(首部 + 数据)进行错误检测,还包括伪首部(含源 IP、目的 IP 等) |
| Urgent Pointer | 16位 | 紧急指针。只有当 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的核心属性
| 属性名 | 类型 | 说明 | 示例 |
|---|---|---|---|
.name | str | 协议名称 | 'Ethernet', 'IP', 'TCP' |
.time | float | 包的时间戳 | 1645234567.123456 |
.len | int | 包的总长度 | 1518 |
.payload | Packet | 下一层协议数据2 | pkt.payload |
.underlayer | Packet | 上一层协议数据 | pkt.underlayer |
.src | str | 源地址3 | 因层而异 |
.dst | str | 目的地址 | 因层而异 |
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