- 网络安全
- 网络
- IDS
【免费下载链接】zeek
Zeek is a powerful network analysis framework that is much different from the typical IDS you may know.
导读
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 解析器有两大特点:
- 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++ 实现。 - 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)。
日志字段完整列表如下:
| 字段 | 类型 | 说明 |
|---|---|---|
ts | time | 该条记录首个 QUIC 包的时间戳 |
uid | string | 连接的唯一 ID |
id | conn_id | 连接的四元组(两端地址/端口) |
version | string | 客户端首个 INITIAL 包中的 QUIC 版本,通常为"1"或"quicv2",其余取值见QUIC::version_strings |
client_initial_dcid | string(可选) | 客户端使用的首个 Destination Connection ID,随机且不可预测,用于客户端与服务器的包保护 |
client_scid | string(可选) | 首个 INITIAL 包中客户端的 Source Connection ID |
server_scid | string(可选) | 服务器选定的 Connection ID(通常取自服务器首个 INITIAL 包),客户端后续包将使用它 |
server_name | string(可选) | 从 ClientHello 的 SNI 扩展提取的服务器名(如可用) |
client_protocol | string(可选) | 从 ClientHello 的 ALPN 扩展提取的首个协议(如可用) |
history | string | QUIC 历史标记串,默认"" |
history_state | vector of string | history 字段的内部状态(非日志列) |
logged | bool | 该记录是否已写入日志(非日志列) |
history 字段字母表
history是 QUIC 日志最有信息量的字段,以字母串形式压缩记录连接生命周期中观测到的包类型。字母含义(客户端发送的字母大写,服务器发送的字母小写):
| 字母 | 含义 |
|---|---|
| I | INIT 包(Initial) |
| H | HANDSHAKE 包 |
| Z | 0-RTT 包 |
| R | RETRY 包 |
| C | CONNECTION_CLOSE 包 |
| S | SSL 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.
相关推荐
Zeek QUIC 分析器与 quic.log 日志字段完全解读
Zeek QUIC 分析器与 quic.log 日志字段完全解读 QUIC 协议将加密、流多路复用与流量控制整合进传输层,并默认使用 TLS 1.3,导致传统
网络安全网络IDSZeek POP3 协议分析器事件接口完全指南:从事件定义到状态机实现
Zeek POP3 协议分析器事件接口完全指南:从事件定义到状态机实现 导读 本文聚焦 Zeek 网络分析框架中 POP3(Post Office Protoc
网络安全网络IDSZeek SMB1 协议分析:`smb1_transaction2_secondary_request` 事件详解与实战指南
Zeek SMB1 协议分析: smb1_transaction2_secondary_request 事件详解与实战指南 本篇技术指南围绕 Zeek 内置 S
网络安全网络IDS
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考