近期在处理一份 CentOS 7 服务器上抓回来的 QUIC 流量包,pcap 文件两百多兆,用老版本 tshark 一看,全是 UDP 443 的包,看不出任何 HTTP/3 的内部信息。后来我用 pyshark 把 tshark 的 QUIC 解析能力接进来,才算把连接 ID、版本号、包号这些关键信息批量抽了出来。如果你也遇到类似的场景——分析机是 CentOS 7,手里有大量 QUIC 流量,又不想一帧一帧点图形界面,这篇内容可以帮你把整个解析流程直接跑通。下面我会从报文结构、环境准备、离线解析、流量解密、实时抓包到踩坑清单,按实际使用顺序写,适合有一定 Python 基础但没深入碰过 pyshark 的人。
1. QUIC 报文解析难点在哪:先搞清楚要解析的内容
1.1 QUIC 的三层结构:UDP、包头、加密负载
QUIC 跑在 UDP 之上,默认使用 443 端口。它和 TCP 类协议完全不一样,没有一个固定格式的头部让你按偏移量依次读字段。按 RFC 9000 的设计,一个 QUIC 数据包首先要区分长包头和短包头。长包头出现在连接刚建立的阶段,Initial、Handshake、0-RTT 都使用长包头,里面携带 Packet Type、版本号、源连接 ID、目标连接 ID,以及 Packet Number 长度等元数据。短包头是连接稳定之后 1-RTT 包使用的,结构更紧凑,通常只有 1-RTT Flag、目标连接 ID 和 Packet Number,很多字段在加密负载里面。
这里最麻烦的地方在于,QUIC 的所有负载都经过 AEAD 加密。不做解密的情况下,你能看到的只有包头里的连接 ID、版本号、包号和长度,内层 CRYPTO 帧、STREAM 帧、HTTP/3 的 QPACK 头部块全部不可读。所以“解析 QUIC 报文”这个说法,在实际工作中至少要分成两个层次:第一个层次是连接级字段解析,不依赖密钥;第二个层次是内容级解密解析,依赖 TLS 密钥日志。这两者的工具链几乎一样,但预期结果完全不同。
1.2 pyshark 与 tshark 的分工边界
很多初学者会把 pyshark 当成一个“能直接读懂 QUIC 的数据库”,其实不是。pyshark 本质上是一个 Python 封装器,它本身不认识任何协议,所有解析能力都来自 tshark。你在 Python 里调用 FileCapture 时,pyshark 会在后台执行一条 tshark 命令,比如tshark -r 文件 -T json,然后把 tshark 输出的 JSON 或 PDML 数据包装成 Packet、Layer 对象。
也就是说,你能解析到哪一层,完全由 tshark 版本决定。CentOS 7 系统默认源里的 tshark 版本偏老,有些版本甚至还没有把 QUIC 当成独立的协议层识别,这种情况下 pyshark 的代码写得再漂亮也没有用。我见过不少人在这块绕了一天弯子,最后发现是 tshark 太老。所以后面的环境准备部分,我会把 tshark 版本单独拎出来讲,这是整条链路的地基。
1.3 能解析到什么程度,取决于你手里有没有密钥
没有密钥时,用 pyshark 能稳定拿到的字段包括:协议层标识 QUIC、版本号、目标连接 ID、源连接 ID、Packet Number、包长度。这个粒度已经足够做很多事了,比如统计同一对连接 ID 出现的频次、判断 QUIC 版本分布、分析连接迁移、查看包号是否连续。做安全基线、异常流检测、粗粒度访问统计,不需要解密钥。
但要看到 QUIC 内层的 CRYPTO、STREAM 帧,或者还原出 HTTP/3 请求路径,就必须先拿到 SSLKEYLOGFILE。密钥日志就相当于 TLS 解密的钥匙,没有它,内容永远是密文。所以建议你在动手写脚本前先想清楚:这次只需要连接统计,还是需要请求级分析。决定越早,后面代码越简单。
2. CentOS 7 上把解析环境搭起来:Python 3、tshark 和权限缺一不可
2.1 Python 3 环境准备
CentOS 7 默认的 python 命令指向 2.7,但 pyshark 的新版本早就放弃 Python 2,如果你直接在系统里执行pip install pyshark,很可能会装进 Python 2.7 环境,跑起来遇到编码和语法兼容问题。更稳妥的做法是先确认 Python 3 是否可用,然后建一个独立的虚拟环境。
python3 -m venv /opt/quic_env source /opt/quic_env/bin/activate pip install --upgrade pip pip install pyshark如果系统里没有 python3,先执行yum install -y python3。有些精简版系统还缺 venv 模块,需要额外安装 python3-venv 或 python36-virtualenv。安装完后,我习惯用which python3 && python3 --version确认版本路径。隔离内网环境如果没法直接访问 PyPI,就把 pyshark 的 wheel 包离线同步进去,再用pip install local.whl安装,这一步别漏。
2.2 tshark 的版本是解析 QUIC 的关键
pyshark 安装完成后,先检查 tshark 到底能不能识别 QUIC。打开终端执行:
tshark -r quic.pcap -Y quic -T fields -e quic.version | head如果命令直接报“No such filter”,或者输出为空,说明当前 tshark 不认识 QUIC 协议层,需要升级。CentOS 7 自带源里的 Wireshark 版本比较旧,最可控的方式是安装第三方 RPM 源里的新版 wireshark-cli,或者源码编译。源码编译对 QUIC 支持最完整,而且可以只编命令行工具,不需要图形界面。编译依赖大致如下:
yum install -y gcc cmake bison flex libpcap-devel glib2-devel gnutls-devel拿到 Wireshark 源码包后,解压并创建 build 目录:
mkdir build && cd build cmake -DBUILD_wireshark=OFF -DBUILD_tshark=ON -DBUILD_gui=OFF -DBUILD_editcap=ON -DBUILD_capinfos=ON .. make -j4 && make install export PATH=/usr/local/bin:$PATH编译时间取决于机器性能,一般几分钟到十几分钟。编译结束后,重新执行tshark -v,确认版本至少是 3.2 以上。QUIC 的完整解析和解密支持在 3.2 版本之后才比较成熟,低于这个版本建议直接升级,不要浪费时间在兼容上。
2.3 pyshark 安装与 tshark 路径问题
pyshark 默认从 PATH 环境变量找 tshark。如果你把新版 tshark 编译到了 /usr/local/bin,但 PATH 里系统自带路径排前面,pyshark 仍可能找到旧版本,导致解析行为不一致。我一般会显式指定 tshark 路径,简单直接:
import pyshark capture = pyshark.FileCapture( 'quic.pcap', tshark_path='/usr/local/bin/tshark' )另外一个容易忽略的文件是 dumpcap。pyshark 在实时抓包模式下会调用 dumpcap 执行真正的采集,离线解析 pcap 时则不需要。如果实时模式提示找不到 dumpcap,先确认 wireshark-cli 是否完整,或者直接用 root 身份运行脚本。我遇到过 dumpcap 存在但没有执行权限的情况,检查一下文件权限和 sudo 的 secure_path 就能定位。
3. 离线 pcap 解析实操:从 UDP 原始包到 QUIC 字段
3.1 一个能直接跑通的脚本
环境准备好之后,离线解析 QUIC pcap 的脚本非常简单。下面这个是我最常用的版本,加了容错,不会因为某个包字段缺失而中断:
import pyshark pcap_path = '/data/quic.pcap' capture = pyshark.FileCapture( pcap_path, display_filter='quic', use_json=True, tshark_path='/usr/local/bin/tshark' ) for pkt in capture: if 'QUIC' not in pkt: continue q = pkt.quic version = getattr(q, 'version', 'N/A') dcid = getattr(q, 'dcid', 'N/A') scid = getattr(q, 'scid', 'N/A') pkt_num = getattr(q, 'packet_number', 'N/A') print(f'{pkt.number} | {version} | {dcid} | {scid} | {pkt_num}') capture.close()这里用getattr而不是直接访问q.version,是因为短包头包不一定有 version 和 scid 字段,直接访问会抛 AttributeError。display_filter 参数让 tshark 只输出匹配 QUIC 的包,Python 端再判断一次'QUIC' in pkt属于双保险。
关于use_json=True要说明一下:较新的 tshark 和 pyshark 组合默认支持 JSON 输出,解析速度快。如果运行时报 JSON 相关错误,说明 tshark 版本太老,把 use_json 参数去掉,pyshark 会退回 PDML 模式。
3.2 常用 QUIC 字段与统计思路
在 Windows 上打开 Wireshark 图形界面看 QUIC 字段很方便,但 Linux 服务器上批量统计时,最好还是用命令行字段列表。查看当前 tshark 支持的所有 QUIC 字段,可以执行:
tshark -G fields | grep -i quic这个命令是排查字段名的第一入口,比翻 pyshark 源码或网上博客快得多。实际工作中,我常用这几个字段:
| 需求 | tshark 字段名 | pyshark 对象属性 |
|---|---|---|
| QUIC 版本 | quic.version | q.version |
| 包类型 | quic.long_packet_type | q.long_packet_type |
| 目标连接 ID | quic.dcid | q.dcid |
| 源连接 ID | quic.scid | q.scid |
| 包号 | quic.packet_number | q.packet_number |
| QUIC 包长度 | quic.packet_length | q.packet_length |
| 帧类型 | quic.frame.type | q.frame.type |
统计 QUIC 连接时,单纯用 dcid 做 key 不够严谨,因为连接迁移和版本协商可能导致连接 ID 变化。更稳妥的组合是(源IP, 源端口, 目的IP, 目的端口, dcid)。利用 pyshark 可以同时拿到 IP 层的字段,例如pkt.ip.src、pkt.udp.srcport,把它们组合起来形成连接维度数据。
3.3 批量处理多个文件和内存控制
FileCapture 本身是惰性迭代,读一个包解析一个包,但如果你习惯把 packet 对象塞进 List,再统一处理,几百 MB 的 pcap 能直接把内存吃满。我处理大文件的方案是优先用apply_on_packets:
def process_packet(pkt): if 'QUIC' not in pkt: return # 提取字段并写入 CSV pass capture.apply_on_packets(process_packet)更粗暴但也非常有效的方式是先在外面用 tshark 对大 pcap 做一次预过滤,只保留 QUIC 包,再交给 pyshark:
tshark -r big.pcap -Y quic -w quic_only.pcap这一步能显著降低后续脚本的内存和 CPU 压力。对于 1GB 级别的 pcap,我通常会按源 IP 或时间段切分成几个小文件,再用 multiprocessing 并行处理,每个子进程独立创建 FileCapture,最后汇总结果。这样比单线程跑完整文件快得多。
4. 解密 QUIC 流量:用 SSLKEYLOG 还原内部信息
4.1 SSLKEYLOGFILE 从哪里来
想看到 QUIC 内层内容,必须先拿到 TLS 密钥。最容易操作的方式是利用客户端的 SSLKEYLOGFILE 环境变量。很多支持 TLS/QUIC 的客户端都会识别这个环境变量,把每次握手的主密钥写进日志。生成方式类似这样:
export SSLKEYLOGFILE=/data/quic_keys.log curl --http3 https://quic.example.com/ > /dev/null这里要求 curl 的编译版本启用了 HTTP/3 和密钥导出能力。如果你的 curl 不支持--http3,换用新版浏览器访问目标网站,同样可以在启动进程前设置该环境变量。keylog 文件里的每一行都是以CLIENT_RANDOM或SECRET_TYPE开头,Wireshark/tshark 读到这些行之后才能进行 TLS 1.3 解密。
抓包和生成 keylog 最好在同一台机器上完成,这样时间线完全一致。如果分开抓包,还需要额外注意密钥是否对应同一个连接会话,排查起来会很麻烦。我实际做法是:先在分析机上启动 tcpdump 抓 443 端口,然后立即在另一终端启动带 SSLKEYLOGFILE 的客户端发起访问,最后统一保存 pcap 和 keylog。
4.2 在 pyshark 里挂载密钥文件
pyshark 的 FileCapture 自带解密参数,不过不同版本参数写法略有差异。常见写法:
capture = pyshark.FileCapture( 'quic.pcap', tls_keylog='/data/quic_keys.log', decrypt_tls=True, use_json=True, tshark_path='/usr/local/bin/tshark' )如果当前 pyshark 版本不认 tls_keylog 和 decrypt_tls,就用自定义参数,本质上等同于向 tshark 传-o tls.keylog_file:...:
capture = pyshark.FileCapture( 'quic.pcap', custom_parameters=['-o', 'tls.keylog_file:/data/quic_keys.log'], use_json=True, tshark_path='/usr/local/bin/tshark' )不管走哪种写法,核心都是 tshark 能否解密。遇到解密没有任何效果时,先在命令行直接验证:
tshark -r quic.pcap -o tls.keylog_file:/data/quic_keys.log -Y 'quic && http3' -T fields -e http3.frame_type -e quic.packet_number | head如果命令行能输出 HTTP/3 层字段,说明 tshark 解密链路没问题,问题出在 pyshark 参数传递上;如果命令行都没有输出,那就去检查 keylog 和 tshark 版本。
4.3 解密后能看到的字段与验证方法
解密成功最直观的表现是 pkt.highest_layer 从 QUIC 变成了 HTTP3,或者 pkt 对象里出现了名为 http3 的层。你可以在循环里加一段检查代码:
for pkt in capture: if 'QUIC' not in pkt: continue print(pkt.highest_layer) if hasattr(pkt, 'http3'): print(pkt.http3._all_fields)HTTP/3 的帧类型、流 ID、QPACK 头部块,在解密后都可以读到。不过要注意,不是每个 QUIC 包都会带上层内容,Initial 握手包、ACK-only 包、连接迁移包都没有 HTTP/3 层,所以看到一部分包 uppermost 层仍是 QUIC 是正常的。如果大量包出现quic.decryption_error字段,多半是 keylog 文件不匹配,或者抓包时漏了最开始的握手过程。
5. 实时抓包解析:LiveCapture 的可行方案与限制
5.1 一个能直接监控 QUIC 连接的脚本
除了离线解析,pyshark 还支持实时抓包解析。下面这个脚本可以持续监控某个网卡上经过的 QUIC 连接,并输出包号和目标连接 ID:
import pyshark capture = pyshark.LiveCapture( interface='ens160', bpf_filter='udp port 443', display_filter='quic', use_json=True, tshark_path='/usr/local/bin/tshark' ) for pkt in capture.sniff_continuously(packet_count=0): if 'QUIC' not in pkt: continue q = pkt.quic dcid = getattr(q, 'dcid', 'N/A') pkt_num = getattr(q, 'packet_number', 'N/A') print(f'{pkt.sniff_time} | packet={pkt_num} | dcid={dcid}')这里有两个过滤器容易混淆。bpf_filter是内核层面的 BPF 过滤器,在抓包阶段就把非 443 UDP 流量挡掉;display_filter是 tshark 的显示过滤,在解析完成后进行一次筛选。前者省资源,后者省 CPU,两者配合使用效果最好。
5.2 权限、网卡和 BPF 过滤器
实时抓包最容易踩的坑是权限。普通用户没有读取 raw socket 的权限,tshark 会报权限错误。最简单的解决方案是直接以 root 运行脚本。如果你希望非 root 用户也能抓包,可以给 dumpcap 设置网络捕获能力:
setcap cap_net_raw,cap_net_admin=eip /usr/local/bin/dumpcap另外,CentOS 7 的网卡命名经常是 ens160、ens33,不一定是 eth0。先用tshark -D查看可用接口列表,再把名字填进 interface 参数,别想当然。BPF 过滤器写成udp port 443是标准的,能过滤掉绝大多数非 QUIC 流量,但如果局域网里还有其他基于 UDP 443 的应用,就需要进一步用限定条件过滤,比如只抓指定主机之间的流量。
5.3 为什么我不建议实时解密
我最初也试过在 LiveCapture 里挂 tls_keylog 参数,结果很尴尬:前面几个包能正常解出 HTTP3 层,几秒钟之后又变回密文。原因是 tshark 进程在启动时会加载 keylog 文件,但并不会持续监控文件新增内容。也就是说,当客户端建立新连接并写入新的密钥行时,已经运行的 tshark 很可能读不到,实时解密也就失效了。
不是 pyshark 的问题,而是底层 tshark 行为如此。所以我的建议是:需要看 HTTP/3 内容时,不要实时解密,先落盘再离线分析。实时模式只用来监控连接级信息:
tcpdump -i ens160 -s 0 -w quic_live.pcap 'udp port 443'抓一段时间后,再把 pcap 文件交给第四章的离线解密流程。这套组合在数据完整性和可重放性上都更可靠。
6. 踩坑记录:版本兼容、字段缺失、解析不出来的三张清单
6.1 版本不匹配的典型症状与处理
我自己把常见问题整理成了一张表,遇到类似报错可以直接对照:
| 症状 | 常见原因 | 处理方式 |
|---|---|---|
| pyshark 启动后立刻退出,没有任何 packet | 当前 tshark 不识别 quic 过滤器 | 升级 tshark,确认版本大于 3.2 |
报错tshark: There is no field named quic.version | 字段名在当前 tshark 版本中不存在 | 执行tshark -G fields | grep quic查真实字段名 |
| 报 JSON 解析相关错误 | tshark 版本太老,不支持-T json | 去掉use_json=True,或升级 tshark |
AttributeError: 'Packet' object has no attribute 'quic' | 没有匹配到 QUIC 包,或 tshark 太老 | 确认 pcap 里确实有 QUIC,并检查 tshark 识别结果 |
| pyshark 一直调用旧版 tshark | PATH 中系统路径优先 | 显式传tshark_path,或调整 PATH 顺序 |
第三行 JSON 错误是很多新手容易忽略的。pyshark 在较新版本里默认可能尝试 JSON 模式,但旧 tshark 根本不支持-T json,只支持 PDML。遇到这种情况,先升级 tshark 永远比改代码省事。
6.2 字段找不到的另一个原因:包类型差异
有时候 tshark 版本没问题,py 脚本还是会因为字段不存在而中断,原因通常是包类型差异。比如短包头包没有 version 字段,也没有 scid 字段;某些 Initial 包可能没有 packet_number,因为包号需要结合版本和长度一起计算。所以我在前面代码里反复强调使用getattr(q, '字段名', 'N/A'),这是严谨的做法。
还有一个细节:pyshark 的 layer 对象只会保留 tshark 输出里非空字段,空值字段会被丢弃。这意味着你不能指望访问一个字段就一定拿到值。如果需要看当前包到底输出了哪些 QUIC 字段,可以直接打印q._all_fields,这是一个字典,key 是字段名,value 是字段值。排查字段问题时,这个属性比 dir() 好用得多。
6.3 性能问题:超大 pcap 和打印开销
分析超大 pcap 时,最影响性能的往往不是解析本身,而是 Python 端的字段转字符串操作。不要轻易执行str(pkt),它会把整个包的所有层全部转成字符串,一次操作可能比解析十个包还慢。只读取你真正需要的字段,也不要在循环里反复打印大段内容。
另外,display_filter='quic'并不能减少 tshark 底层读包数量,它只是在输出端过滤。如果 pcap 里绝大多数包都不是 QUIC,先用命令行把 quic 子集抽出来,再让 pyshark 处理,性能会有数量级提升。文件处理完记得调用capture.close(),否则句柄和内存占用会一直留在进程里,多个文件连续处理时尤其明显。
7. 最后一点实战心得:把 QUIC 解析封装成一个可复用脚本
环境、原理、踩坑都讲完之后,我把自己的常用逻辑封装成一个简单函数,方便日后直接调用。代码不复杂,但把需要注意的点都收敛到一起:
import pyshark def parse_quic(pcap_path, keylog=None, tshark_path='/usr/local/bin/tshark'): kwargs = { 'tshark_path': tshark_path, 'display_filter': 'quic', 'use_json': True, } if keylog: kwargs['custom_parameters'] = ['-o', f'tls.keylog_file:{keylog}'] capture = pyshark.FileCapture(pcap_path, **kwargs) result = [] def on_packet(pkt): if 'QUIC' not in pkt: return q = pkt.quic info = { 'frame': pkt.number, 'time': str(pkt.sniff_time), 'version': getattr(q, 'version', ''), 'dcid': getattr(q, 'dcid', ''), 'scid': getattr(q, 'scid', ''), 'packet_number': getattr(q, 'packet_number', ''), 'highest_layer': pkt.highest_layer, } result.append(info) capture.apply_on_packets(on_packet) capture.close() return result封装之后,密钥文件、tshark 路径、过滤条件都通过参数控制,不会散落在脚本各处。我这边从环境搭建到跑通解析流程,前后花了一个下午,大头时间几乎都耗在 tshark 版本上。如果你机器上的版本低于 3.2,不用多想,先升级。依赖一旦对齐,pyshark 解析 QUIC 就只是普通的 Python 循环而已。