☰
Zeek 连接处理机制深度解析:校验和验证与连接方向翻转
2026/9/28 20:52:28 网站建设 项目流程
  • 网络安全
  • 网络
  • 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
点击查看免费下载

Zeek(原 Bro)作为一款强大的网络分析框架,其对连接(connection)的追踪与建模贯穿了从底层 C++ 引擎到 Zeek 脚本层的整个体系。本文聚焦 Zeek 开发者文档 Connection Handling 中的两大核心主题:校验和(checksum)行为与连接翻转(flipping)。通过阅读本文,你将理解为何损坏校验和的报文仍可能出现在conn.log中但计数为零、掌握history字段中c/C与^标记的底层含义,并能从源码层面解释 Zeek 如何决定连接的 originator(发起方)与 responder(响应方),以及这一决策在分析器树(analyzer tree)中的传递机制。

一、校验和(Checksum)行为:从"直接丢弃"到"先建连后校验"

1.1 默认行为概览

默认情况下,Zeek 会忽略绝大多数携带无效校验和的报文("mostly ignore packets that have invalid checksums")。这里的关键词是"mostly"——不同层的校验和错误处理方式并不相同:

  • IPv4 头校验和错误:Zeek 产生bad_IP_checksumweird,并在连接查找步骤之前直接丢弃该报文;
  • L4 校验和错误(TCP、UDP、ICMP 等):自 Zeek 8.2 起,Zeek仅在利用潜在损坏的 L4 头部信息完成连接的查找或创建之后,才判断 L4 校验和是否无效。

这一顺序差异是理解后续所有行为的根基:L4 校验和错误的报文仍然参与了连接查找/创建,只是不参与连接的分析与统计。

1.2 源码链路:连接查找与校验分离

文档明确指出,连接查找实现在IPBasedAnalyzer::AnalyzePacket()中,校验验证则发生在各协议专属分析器的DeliverPacket()中。从源码结构看,这条链路清晰可循:

  1. 连接查找:在 src/packet_analysis/protocol/ip/IPBasedAnalyzer.cc 的IPBasedAnalyzer::AnalyzePacket()中,Zeek 先通过InitConnKey()构造连接键(IPBasedConnKey),随后调用session_mgr->FindConnection(*key)查找已有连接,未找到则调用NewConn()创建新连接。此时尚未进行任何 L4 校验和验证。
  2. IPv4 校验和检查:在更早的 IP 层处理中,src/packet_analysis/protocol/ip/IP.cc 会对 IPv4 头校验和进行检查——当! packet->l3_checksummed && ! ignore_checksums && ... in_cksum(...) != 0xffff时,产生Weird("bad_IP_checksum", packet)并直接返回false,报文被丢弃,不会进入连接查找阶段。这与文档描述的"IPv4 校验和错误在连接查找前丢弃"完全一致。
  3. L4 校验和验证:连接建立后,才在各协议层验证 L4 校验和:
    • TCP:TCPAnalyzer::ValidateChecksum()(src/packet_analysis/protocol/tcp/TCP.cc)在满足! l4_checksummed && ! ignore_checksums && ! GetIgnoreChecksumsNets()->Contains(ip->IPHeaderSrcAddr())等条件时调用endpoint->ValidChecksum()验证,失败则产生bad_TCP_checksumweird 并调用endpoint->ChecksumError();
    • UDP:UDPAnalyzer::DeliverPacket()(src/packet_analysis/protocol/udp/UDP.cc)中,先调用adapter->DeliverPacket(...)把报文交给会话适配器(保证packet_contents等事件仍能拿到数据),随后才进行校验和验证;失败时调用adapter->HandleBadChecksum(is_orig)(src/packet_analysis/protocol/udp/UDPSessionAdapter.cc),该函数产生bad_UDP_checksumweird 并记录历史,同时TapPacket(pkt, PacketAction::Skip, SkipReason::BadChecksum)将报文标记为跳过。

值得注意的细节:UDP 分析器中存在针对 VXLAN 的特殊处理——当目的端口是已知 VXLAN 端口且校验和字段为零时跳过校验(src/packet_analysis/protocol/udp/UDP.cc),这体现了"校验和可选"协议的现实考量。

1.3 校验和错误对统计与历史的双重影响

校验和验证失败的关键后果是:

  1. 不计入连接统计:无效校验和的报文不会被计入连接的orig_pkts或resp_pkts,也不会传递给连接的协议分析器。
  2. 写入历史字段:报文的c(responder 方向的小写)或C(originator 方向的大写)被以**对数方式(logarithmic fashion)**追加到连接的 history 字段。

关于history字段的语义,scripts/base/protocols/conn/main.zeek 中给出了权威说明:c表示 "packet with a bad checksum (applies to UDP too)",来自 originator 的事件记为大写,来自 responder 的记为小写;c/g/t/w等类型采用对数记录方式——第二次出现代表该事件至少发生了 10 次,第三次代表 100 次,依此类推。

其底层实现为Session::ScaledHistoryEntry()(src/session/Session.cc):内部维护一个计数器与缩放阈值,每当++counter == scaling_threshold时写入历史字符,并将阈值乘以缩放基数(默认 10),实现"以对数步长压缩高频事件"的效果。TCP 端点的ChecksumError()(src/analyzer/protocol/tcp/TCP_Endpoint.cc)与 UDP 会话适配器的HandleBadChecksum()(src/packet_analysis/protocol/udp/UDPSessionAdapter.cc)正是通过该机制分别写入C/c历史。当跨越阈值时,还会触发tcp_multiple_checksum_errors(src/analyzer/protocol/tcp/events.bif)或udp_multiple_checksum_errors(src/packet_analysis/protocol/udp/events.bif)事件,供脚本层响应。

1.4 实战验证:零包计数的 conn.log

文档给出了两个可直接复现的实验命令。所用抓包文件均存在于本仓库的 testing/btest/Traces 目录下(Traces/tcp/syn-bad.pcap、Traces/dns/dns-corrupt.pcap,其全局路径分别为 testing/btest/Traces/tcp/syn-bad.pcap 与 testing/btest/Traces/dns/dns-corrupt.pcap)。

TCP 场景:回放坏校验和的 SYN 报文,以 JSON 格式输出conn.log:

# zeek -D -b -r Traces/tcp/syn-bad.pcap base/protocols/conn LogAscii::use_json=T # jq < conn.log { "ts": 1362692526.869344, "uid": "CJKFoj4bpHEhTeaRoj", "id.orig_h": "141.142.228.5", "id.orig_p": 59856, "id.resp_h": "192.150.187.43", "id.resp_p": 80, "proto": "tcp", "conn_state": "OTH", "local_orig": false, "local_resp": false, "missed_bytes": 0, "history": "C", "orig_pkts": 0, "orig_ip_bytes": 0, "resp_pkts": 0, "resp_ip_bytes": 0, "ip_proto": 6 }

UDP 场景:回放坏校验和的 DNS 报文:

# zeek -b -r Traces/dns/dns-corrupt.pcap base/protocols/conn LogAscii::use_json=T # jq < conn.log { "ts": 1777450586.006844, "uid": "CJKFoj4bpHEhTeaRoj", "id.orig_h": "192.168.0.109", "id.orig_p": 34357, "id.resp_h": "8.8.8.8", "id.resp_p": 53, "proto": "udp", "conn_state": "OTH", "local_orig": true, "local_resp": false, "missed_bytes": 0, "history": "Cc", "orig_pkts": 0, "orig_ip_bytes": 0, "resp_pkts": 0, "resp_ip_bytes": 0, "ip_proto": 17 }

两个示例的history字段分别为C与Cc,而orig_pkts、resp_pkts均为 0。这说明:校验和错误的报文成功创建/匹配了连接并留下历史痕迹,但从未被计入任何方向的数据包统计——这正是"先建连后校验"设计的最直接体现。conn_state为OTH也符合预期:由于没有有效数据包参与状态机推进,连接停留在"无状态"阶段。

从conn.log的字段定义看(scripts/base/protocols/conn/main.zeek),orig_pkts、orig_ip_bytes、resp_pkts、resp_ip_bytes均标注 "Only set ifuse_conn_size_analyzer= T",即由ConnSize_Analyzer分析器(见下文)负责填充。相关讨论可追溯至 Zeek 的 issue #5277(bad checksum处理机制变更)。

1.5 相关配置项

校验和验证并非无条件执行,Zeek 提供了两个脚本层开关(定义于 scripts/base/init-bare.zeek):

  • ignore_checksums: bool(默认F):置为T可全局跳过所有校验和验证;
  • ignore_checksums_nets: set[subnet](默认空集):指定网段的报文跳过校验和验证,适用于已知存在网卡 offload 问题的网络环境。

这两个开关在 src/packet_analysis/protocol/ip/IPBasedAnalyzer.cc 中被加载(GetIgnoreChecksumsNets()),并被 TCP/UDP 校验和验证逻辑引用;scripts/base/packet-protocols/ip/main.zeek 中还有对应的选项变更处理器,运行时修改ignore_checksums_nets会即时同步到底层。

二、连接翻转(Flipping):originator 与 responder 的角色再定义

2.1 核心概念:谁先发谁就是 originator?

Zeek 以originator(发起方)与responder(响应方)的双端模型描述连接。这一概念在 Zeek 脚本层体现为is_orig: bool事件参数,在底层 C++ API 中体现为Connection实例的访问器——如OrigAddr()/RespAddr()、OrigPort()/RespPort()。

通常情况下,连接的第一个报文决定哪一端是 originator、哪一端是 responder。但存在一个特例:当首包的源端口位于likely_server_ports集合中时(意味着源端口像是一个服务端端口,例如 80、443、53),Zeek 会翻转这一判定,并在连接的 history 中追加^(caret)标记。likely_server_ports定义于 scripts/base/init-bare.zeek,其典型语义是"该端口上更可能是服务端";scripts/base/frameworks/analyzer/main.zeek 中还会自动将各分析器声明的server_ports合并进likely_server_ports。

2.2 源码视角:翻转发生在哪里?

连接翻转的判断发生于IPBasedAnalyzer::NewConn()(src/packet_analysis/protocol/ip/IPBasedAnalyzer.cc):

  1. 调用虚函数WantConnection(src_p, dst_p, payload, flip)询问各协议分析器是否接受该连接,以及是否需要翻转角色;
  2. 各协议通过IsLikelyServerPort()(src/packet_analysis/protocol/ip/IPBasedAnalyzer.cc)查询likely_server_ports表(该表被缓存以加速查找);
  3. 例如 UDP 分析器的WantConnection()(src/packet_analysis/protocol/udp/UDP.cc)即实现为flip_roles = IsLikelyServerPort(src_port) && ! IsLikelyServerPort(dst_port);
  4. 若flip为真且响应地址不是广播地址(! conn->RespAddr().IsBroadcast()),则调用conn->FlipRoles()。

翻转动作的核心实现在Connection::FlipRoles()(src/Conn.cc),它依次:

  • 交换orig_addr/resp_addr、orig_port/resp_port,以及 L2 地址、流标签等端点属性;
  • 将conn_val(脚本层的connection记录)中的id记录与端点字段同步交换;
  • 调用key->FlipRoles()让连接键(IPBasedConnKey)同步更新;
  • 调用adapter->FlipRoles()递归翻转会话适配器;
  • 通过analyzer_mgr->ApplyScheduledAnalyzers(this)重新调度分析器;
  • AddHistory('^')在 history 中记录翻转标记;
  • 触发connection_flipped事件(若注册了该事件处理器)。

值得一提的是,IPBasedConnKey类本身就持有flipped成员并提供FlipRoles()API(src/packet_analysis/protocol/ip/conn_key/IPBasedConnKey.cc):在InitTuple()中,若addr_port_canon_lt(...)判定源端小于目的端(按规范序比较),则按原样存储并将flipped置为false;否则交换存储并置为true。SrcAddr()/DstAddr()、SrcPort()/DstPort()的取值因此会依据flipped标志动态决定(src/packet_analysis/protocol/ip/conn_key/IPBasedConnKey.h)。

2.3 翻转如何渗透各层:分析器树的递归 FlipRoles

翻转并非只发生在连接层——它需要"渗透"到整个分析器树(analyzer tree)。文档强调:

"the Analyzer API offers a virtualFlipRoles()method that is executed recursively on the analyzer tree when endpoint flipping happens. All analyzers have to update their internal state upon such an event."

这一机制在 src/analyzer/Analyzer.cc 中实现:Analyzer::FlipRoles()会对其子分析器递归调用FlipRoles()。以文档举例的ConnSize_Analyzer(追踪各方向数据包与字节计数的分析器)为例,其FlipRoles()(src/analyzer/protocol/conn-size/ConnSize.cc)在调用基类版本后,交换orig_bytes/resp_bytes与orig_pkts/resp_pkts,确保后续DeliverPacket()调用中is_orig语义反转后计数仍然正确。该分析器正是conn.log中orig_pkts、resp_pkts、orig_ip_bytes、resp_ip_bytes字段的填充者(src/analyzer/protocol/conn-size/ConnSize.cc)。

类似的还有HTTP_Analyzer::FlipRoles()(src/analyzer/protocol/http/HTTP.cc)——当翻转发生在协议升级之后时还需处理其内部状态。此外,分析器并非只能在连接建立时触发翻转:例如 DNS 分析器在解析首个消息时发现QR位表明方向与预期相反,会直接调用analyzer->Conn()->FlipRoles()(src/analyzer/protocol/dns/DNS.cc)——这属于运行时、由应用层逻辑驱动的翻转。

2.4 第二包翻转与 stale is_orig 问题

文档指出,翻转通常发生在连接第一个包处理之前,但较新的 Zeek 版本(对应 PR #2191)已支持在第二个包时进行翻转。从技术上讲,任何分析器或逻辑都可以在任何时刻触发翻转——但这会带来一个棘手问题:

"an in-flightForwardStream()orForwardPacket()invocation on a connection's analyzer tree ends-up using a staleis_origparameter."

也就是说,当一次ForwardStream()/ForwardPacket()调用正在分析器树上进行时,如果某个分析器触发了翻转,那么随后对分析器树的DeliverPacket()调用将使用过期的is_orig栈变量。文档中观察到的真实案例即ConnSize_Analyzer:它在TCPSessionAdapter::Process()调用之后被访问,若Process()翻转了连接,ConnSize_Analyzer的DeliverPacket()会拿到错误的is_orig,导致单个包被错误记账。

这一论述在源码层面成立:DeliverPacket()的is_orig作为参数从调用链逐层传入,而ConnSize_Analyzer::DeliverPacket()(src/analyzer/protocol/conn-size/ConnSize.cc)直接依赖is_orig决定累加到orig_*还是resp_*计数器,期间并不重新查询连接的当前端点角色——因此一旦翻转发生在传递路径中途,就会产生上述账目偏差。

2.5 未来展望:从 originator/responder 到 left/right

文档最后提出了一项颇有深度的架构展望:未来 Zeek 可以考虑将最底层设计为对 originator/responder 概念无感知——即始终以确定性规则(如规范化排序)命名端点,例如left和right,而把 originator/responder 的语义上移到更高层实现。

依据在于:IPBasedConnKey类当前已持有flipped成员并暴露FlipRoles()API(src/packet_analysis/protocol/ip/conn_key/IPBasedConnKey.h),这意味着原始连接追踪层已经隐式承担了角色方向的逻辑。文档认为这并不合理——纯连接追踪层不应了解 originator/responder 概念,因为这会引入相当可观的复杂度。这一构想可以理解为:连接键层只负责"稳定、可复现地标识一条流",而方向语义(谁主动、谁被动)交由上层分析框架按需推导。

三、总结与排查实践建议

回到实战层面,理解校验和与翻转机制有助于更准确地解读conn.log:

  1. 看到orig_pkts/resp_pkts全为 0 但 history 含c/C:不要惊讶,这是 Zeek 8.2+ 的预期行为——坏校验和报文参与了连接创建/查找与历史记录,但未参与计数与分析。可结合bad_TCP_checksum、bad_UDP_checksumweird 进一步定位;
  2. 看到 history 中的^:表示 Zeek 根据likely_server_ports或应用层逻辑翻转了连接方向,脚本层可通过connection_flipped事件(定义于 src/Conn.cc 的使用处)感知并做出响应;
  3. 排查网络环境问题:若因网卡 offload 导致大量校验和错误,可评估设置ignore_checksums_nets或ignore_checksums选项,避免噪声影响分析;
  4. 注意计数与字节字段的依赖:orig_pkts等字段仅在启用use_conn_size_analyzer时由ConnSize_Analyzer填充(见 scripts/base/protocols/conn/main.zeek),解读日志前应先确认该开关状态。

更多实现细节可进一步阅读 src/packet_analysis/protocol/ip/IPBasedAnalyzer.cc、src/packet_analysis/protocol/tcp/TCP.cc、src/packet_analysis/protocol/udp/UDP.cc 与 src/Conn.cc,以及 scripts/base/protocols/conn/main.zeek 中的history字段语义说明。

  • 网络安全
  • 网络
  • 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
点击查看免费下载

相关推荐

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

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

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

立即咨询