☰
Zeek QUIC 协议分析完全指南:quic.log 生成机制与自定义事件实战
2026/10/9 1:38:40 网站建设 项目流程
  • 网络安全
  • 网络
  • IDS

【免费下载链接】zeek

Zeek is a powerful network analysis framework that is much different from the typical IDS you may know.

项目地址:https://gitcode.com/gh_mirrors/ze/zeek
点击查看免费下载

导读

QUIC 是 HTTP/3 与 DNS-over-QUIC 的底层传输协议,随着其大规模部署,网络监控与分析工具必须原生支持 QUIC。本文以 Zeek 仓库中 doc/scripts/base/protocols/quic/index.rst 为骨架,系统讲解base/protocols/quic脚本包如何实现 QUIC 分析并生成quic.log,涵盖 QUIC 分析器的四类组成文件、quic.log全部日志字段、QUIC 版本映射表、分析器生成的事件列表,以及基于这些事件编写自定义检测脚本的实战方法。读完本文,你将能读懂 Zeek 的 QUIC 日志语义,并在此基础上编写自己的 QUIC 检测与统计脚本。

一、脚本包概览:base/protocols/quic 的四层结构

Zeek 的 QUIC 分析能力由 scripts/base/protocols/quic/ 目录下的脚本包实现。根据文档索引,该包包含四个核心文件,各自职责如下:

文件职责
load.zeek加载入口,按条件装载其余脚本与 DPD 签名
spicy-events.zeek声明 QUIC 分析器生成的 Zeek 事件(事件接口)
consts.zeek定义 QUIC 版本号到可读字符串的映射表
main.zeek核心实现:日志流定义、事件处理、生成quic.log

四个文件通过@load指令串联:__load__.zeek在Analyzer::ANALYZER_QUIC存在的前提下(@ifdef守卫,避免分析器未编译时脚本加载失败),依次装载spicy-events、consts、main,并加载 DPD 签名文件dpd.sig:

@ifdef ( Analyzer::ANALYZER_QUIC ) @load ./spicy-events @load ./consts @load ./main @load-sigs ./dpd.sig @endif

需要说明的是,dpd.sig并不在该脚本目录中,而是生成于构建过程;DPD(Dynamic Protocol Detection)签名用于在 UDP 端口非标准时动态识别 QUIC 流量。

二、QUIC 分析器的底层实现:Spicy 解析器与解密支持

main.zeek注释明确该包"Implements base functionality for QUIC analysis. Generates quic.log."(实现 QUIC 分析的基础功能并生成 quic.log)。在脚本层之下,真正的 QUIC 报文解析由 Spicy 解析器完成,源码位于 src/analyzer/protocol/quic/QUIC.spicy。

这个 Spicy 解析器有两大特点:

  1. INITIAL 包载荷解密:Zeek 利用 QUIC 规范中 INITIAL 包使用公开已知密钥(well-known keys)加密的特性,在 decrypt_crypto.cc 的 C++ 代码中解密 INITIAL 载荷,从而提取 ClientHello 中的 SNI(服务器名)与 ALPN(应用层协议协商)信息——这正是quic.log中server_name和client_protocol字段的数据来源。Spicy 端通过decrypt_crypto_payload()函数(&cxxname="QUIC_decrypt_crypto_payload")调用该 C++ 实现。
  2. TLS 握手转发:解密后的 CRYPTO 帧数据被重组为 TLSPlaintext 记录(legacy_record_version 设为\x03\x03即 TLS 1.3),转发给 SSL 分析器,从而复用 Zeek 已有的 TLS 事件体系(如ssl_client_hello),这也是main.zeek中能直接用ssl_*事件获取 SNI/ALPN 的原因。

QUIC.spicy中还有一个值得关注的常量:short_header_packet_threshold_base = 2,它与后文history字段中O(short header 包)的"二进制对数方式记录"直接相关。这些解析器文件与脚本的耦合通过 QUIC.evt 事件绑定文件完成,构建系统配置见 CMakeLists.txt。

三、核心实现:quic.log 的完整字段语义

main.zeek中的QUIC::Info记录类型定义了quic.log的每一列。该记录通过Log::create_stream(LOG, Log::Stream($columns=Info, $ev=log_quic, $path="quic", $policy=log_policy))注册为日志流,输出路径为quic(即quic.log)。

日志字段完整列表如下:

字段类型说明
tstime该条记录首个 QUIC 包的时间戳
uidstring连接的唯一 ID
idconn_id连接的四元组(两端地址/端口)
versionstring客户端首个 INITIAL 包中的 QUIC 版本,通常为"1"或"quicv2",其余取值见QUIC::version_strings
client_initial_dcidstring(可选)客户端使用的首个 Destination Connection ID,随机且不可预测,用于客户端与服务器的包保护
client_scidstring(可选)首个 INITIAL 包中客户端的 Source Connection ID
server_scidstring(可选)服务器选定的 Connection ID(通常取自服务器首个 INITIAL 包),客户端后续包将使用它
server_namestring(可选)从 ClientHello 的 SNI 扩展提取的服务器名(如可用)
client_protocolstring(可选)从 ClientHello 的 ALPN 扩展提取的首个协议(如可用)
historystringQUIC 历史标记串,默认""
history_statevector of stringhistory 字段的内部状态(非日志列)
loggedbool该记录是否已写入日志(非日志列)

history 字段字母表

history是 QUIC 日志最有信息量的字段,以字母串形式压缩记录连接生命周期中观测到的包类型。字母含义(客户端发送的字母大写,服务器发送的字母小写):

字母含义
IINIT 包(Initial)
HHANDSHAKE 包
Z0-RTT 包
RRETRY 包
CCONNECTION_CLOSE 包
SSSL Client/Server Hello
U不认识的 QUIC 版本(Unfamiliar version)
X丢弃的包(成功解密 INITIAL 包之后遇到固定位为 0 的包)
O短头包,按二进制对数方式记录数量

例如一条history值可能是Ihzs:客户端 INIT(I)、服务器 Handshake(h)、0-RTT(z)、服务器 SSL Hello(s)。add_to_history()函数按方向将字母大写或小写追加到history_state向量中,达到max_history_length(默认 100,可通过option max_history_length调整)时触发QUIC_max_history_length_reached连接怪异(weird)记录。

四、QUIC 版本映射:version_strings 表

consts.zeek 定义了QUIC::version_strings,把 QUIC 原始 32 位版本号映射为可读字符串:

const version_strings: table[count] of string = { [0x00000001] = "1", [0x6b3343cf] = "quicv2", [0xff000016] = "draft-22", # ... draft-23 至 draft-34 依次为 0xff000017 - 0xff000022 [0xfaceb001] = "mvfst (faceb001)", # ... mvfst 系列 0xfaceb002、0xfaceb00e、0xfaceb011-0xfaceb013 } &default=function(version: count): string { return fmt("unknown-%x", version); };

该表的三个区间覆盖了真实世界的三类 QUIC 版本:

  • 正式发布版:0x00000001(QUIC v1,映射为"1")与0x6b3343cf(QUIC v2,映射为"quicv2");
  • IETF 草案版:0xff000016(draft-22)到0xff000022(draft-34),其中0xff00001d与0xff00001e都映射为"draft-30"(注意 0xff00001f 实际对应 draft-31,但表中没有该值,若出现会走 default 分支);
  • 实现专属版本:Meta 的 mvfst 实现所使用的0xfaceb0xx系列。

任何未在表中的版本号都通过&default匿名函数渲染为unknown-<hex>形式(如unknown-ff0000ff),保证quic.log的version字段永远有可读值。

五、端口注册与 DoQ(DNS-over-QUIC)支持

main.zeek定义了三个与端口相关的常量:

  • quic_ports = { 443/udp }:HTTP/3 over QUIC 的知名端口;
  • doq_ports = { 853/udp, 784/udp }:DNS-over-QUIC 端口(853 为 IANA 分配,784 为早期部署端口)。

在zeek_init()中通过分析器端口注册 API 绑定:

Analyzer::register_for_ports(Analyzer::ANALYZER_QUIC, quic_ports); Analyzer::register_for_ports(Analyzer::ANALYZER_QUIC, set(), doq_ports);

注意doq_ports不会自动加入likely_server_ports,这是为了避免在测试基线中引发 originator/responder 方向的虚假翻转。如果你所在环境需要正确的方向判定,可在local.zeek中手动添加:

redef likely_server_ports += { 853/udp, 784/udp };

quic_ports与doq_ports均为&redef常量,可在local.zeek中覆盖或追加自定义端口。

六、QUIC 分析器事件全清单

spicy-events.zeek 声明了 QUIC 分析器可能生成的全部事件(事件参数对应 QUIC RFC 9000 中的字段定义)。所有事件均以(c: connection, is_orig: bool, ...)开头,其中c为连接对象,is_orig表示包是否来自连接发起方。

事件触发时机额外参数
QUIC::initial_packet观测到 QUIC Initial 包version、dcid、scid
QUIC::retry_packet观测到 QUIC Retry 包version、dcid、scid、retry_token、retry_integrity_tag
QUIC::handshake_packet观测到 QUIC Handshake 包version、dcid、scid
QUIC::zero_rtt_packet观测到 QUIC 0-RTT 包version、dcid、scid
QUIC::connection_close_frame观测到 CONNECTION_CLOSE 帧version、dcid、scid、error_code、reason_phrase
QUIC::unhandled_version遇到未识别的 QUIC 版本version、dcid、scid
QUIC::discarded_packet遇到固定位(fixed bit)为 0 的包,且此前已成功解密过 INITIAL 包total_decrypted(此前成功解密的包数)
QUIC::short_header_packet_threshold_crossed观测到短头包的数量达到某个二进制对数阈值threshold

两个事件需要注意其语义边界:

  • QUIC::connection_close_frame:注释明确指出,连接建立后的 CONNECTION_CLOSE 帧通常是加密的,Zeek 不可见,因此该事件实际只在握手中的明文阶段可见(即main.zeek中"若存在待记录状态则立即落盘并准备新条目"的场景);
  • QUIC::discarded_packet:仅当该连接此前有成功解密的 INITIAL 包时才生成,total_decrypted给出此前的成功解密计数。

七、核心状态机:main.zeek 的事件处理逻辑

main.zeek将上述事件与日志生命周期连接起来,形成一套完整的状态机:

会话建立:QUIC::initial_packet/handshake_packet/zero_rtt_packet事件都会调用set_session():若连接尚无c$quic状态则创建Info记录(记录时间戳、UID、四元组、经version_strings映射的版本),并注册finalize_quic连接移除钩子;同时填充client_initial_dcid(仅发起方首次)、client_scid或server_scid(按方向),随后调用add_to_history()追加对应字母。

异常与闭合路径:retry_packet、unhandled_version、connection_close_frame三个事件处理逻辑一致——补充 history 后立即log_record()落盘,然后delete c$quic清空状态,为同一条 UDP 流上可能出现的下一条 QUIC 连接准备新条目。这对应注释中"同一 UDP 连接上可能出现多个不同 Connection ID 的 QUIC 连接"(XXX 标注)的设计考量。

加密后阶段:QUIC::short_header_packet_threshold_crossed和QUIC::discarded_packet在c$quic缺失时分别产生QUIC_spurious_short_header_packet_threshold_crossed与QUIC_spurious_discarded_packet连接怪异(weird)记录;其中前者对应"没看到 INITIAL 却解析出了短头包"的边界情况。

TLS 数据融合:三个ssl_*事件被复用,将 SSL 分析器的结果写回 QUIC 记录:

  • ssl_extension_server_name(优先级 +5):从 ClientHello 的 SNI 扩展提取server_name;
  • ssl_extension_application_layer_protocol_negotiation:提取client_protocol(仅取首个协议),若协议多于一个则产生QUIC_many_protocols怪异记录——注释说明这是"为避免 vector 或拼接而有意只记录首个"的取舍;
  • ssl_client_hello/ssl_server_hello:分别以大写S/小写s追加 history。

最终落盘兜底:finalize_quic连接移除钩子保证连接结束、且记录尚未写入时(c$quic$logged为假)把剩余状态写入日志,避免数据丢失。

八、自定义检测实战:基于 QUIC 事件编写脚本

理解上述事件后,就可以在策略层编写自定义 QUIC 检测。以下脚本演示三类常见用法:

# 1) 统计并输出每个 QUIC 连接的版本与服务器名 event QUIC::initial_packet(c: connection, is_orig: bool, version: count, dcid: string, scid: string) { if ( is_orig ) print fmt("QUIC session: %s version=%s server=%s", c$uid, QUIC::version_strings[version], c$quic?$server_name ? c$quic$server_name : "-"); } # 2) 监控遇到陌生 QUIC 版本的连接(version_strings 的 default 分支会命中) event QUIC::unhandled_version(c: connection, is_orig: bool, version: count, dcid: string, scid: string) { NOTICE([$note=Weird::Activity, $conn=c, $msg=fmt("Unhandled QUIC version 0x%x observed", version)]); } # 3) 记录 QUIC 连接关闭原因码 event QUIC::connection_close_frame(c: connection, is_orig: bool, version: count, dcid: string, scid: string, error_code: count, reason_phrase: string) { if ( is_orig ) print fmt("QUIC close: %s error_code=%d reason=%s", c$uid, error_code, reason_phrase); }

将该脚本放入local.zeek(scripts/site/local.zeek)或经@load加载即可生效。注意使用c$quic状态字段前应先判断c?$quic(例如只在initial_packet之后的事件中直接访问),并了解max_discarded_packet_events(默认 100,0 表示不限量,-1 表示禁用)等可调参数。由于 QUIC 事件依赖 Spicy 解析器(Analyzer::ANALYZER_QUIC),脚本在未编译 QUIC 支持的构建中会被@ifdef自动跳过。

九、验证与测试

Zeek 的 QUIC 分析器有完整的回归测试支撑。可在仓库的 testing/btest 目录中检索quic相关测试用例,验证quic.log字段语义、history 字母行为以及各事件触发的正确性;QUIC.spicy本身长达数百行,覆盖长头/短头包、CRYPTO 帧重组、版本协商等解析路径,是深入理解解析器行为的首选阅读材料。

总结

Zeek 的 QUIC 支持是"Spicy 解析器 + Zeek 脚本"分层协作的典型范例:Spicy 层(QUIC.spicy、QUIC.evt、decrypt_crypto.cc)负责报文解析、INITIAL 解密与 TLS 转发;脚本层(main.zeek、spicy-events.zeek、consts.zeek)负责把解析结果转化为结构化日志quic.log与可编程事件。理解Info记录的全部字段、history字母表与八类事件的触发时机,是撰写高效 QUIC 检测脚本的基础——这也是该脚本包(对应 doc/scripts/base/protocols/quic/index.rst)为分析者提供的核心价值。

  • 网络安全
  • 网络
  • IDS

【免费下载链接】zeek

Zeek is a powerful network analysis framework that is much different from the typical IDS you may know.

项目地址:https://gitcode.com/gh_mirrors/ze/zeek
点击查看免费下载
上一篇:终极Vortex模组管理器使用指南:从零开始轻松管理游戏模组
下一篇:WLED高级用户手册:250+预设与自定义效果全攻略

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询