用 Zeek SumStats 做连接计数与按主机阈值统计

来源:Zeek 项目文档贡献者,Book of Zeek 9.0.0:Summary Statistics。本文覆盖该章的概念与两个完整示例,依据固定版本文档和官方对应源文件核对,遵循 CC BY 4.0。译注和适用范围提醒已明确标示;没有执行脚本或 PCAP 回放。

用 Zeek 测量网络流量的某个属性,在有限大小的抓包文件上并不难。但真实部署中的流量不会停止,还可能由多个 worker 分担分析。如果不断把原始事件放进一个表,既要面对无限增长的数据,也要处理跨进程聚合。

SumStats(Summary Statistics)把观察、归约和时间窗口分开,让脚本用较小的汇总状态描述持续流量。它适用于单进程 Zeek,也为集群提供透明的合并机制。开发者仍需控制数据规模,并理解自己统计的事件到底代表什么。

Zeek SumStats 流程:事件产生带流名、键和值的观察,Reducer 按键求和,Sumstat 在窗口结束输出结果或在跨越阈值时调用回调。
未完纪自绘,依据 Zeek 9.0.0 文档。一个流可以按全局空键聚合,也可以按来源主机分别聚合。

三个概念与一条数据流

Observation(观察)是一个数据点,包含任意命名的观察流、说明这个点属于谁的键,以及实际观察值。例如,“连接已建立”是流名,空键表示全局统计,每个事件贡献数值 1。

Reducer(归约器)订阅一个观察流,对每个键分别保存计算结果,例如求和、平均值或方差。归约能缩小单个键的数据表示,但并不会自动限制不同键的数量。若每个观察都产生一个新键,内存仍可能增长。

Sumstat(统计定义)把一个或多个 Reducer 放进一个时间窗口(epoch),并配置输出或阈值回调。窗口结束时,可以逐键取得结果;也可以根据结果计算用于阈值判断的值,并在跨越阈值时调用回调。

这套分层让业务事件处理函数只负责“观察到了什么”,而窗口、归约和通知由统计定义管理。在集群中,框架处理由流量分担带来的聚合细节;这并不意味着集群合并没有通信或内存开销。

示例一:每分钟统计已建立的连接

把以下脚本保存为 sumstats-countconns.zeek。它保留官方示例的事件、流名、空键、窗口和回调逻辑,注释与排版作了中文整理。

@load base/frameworks/sumstats

event connection_established(c: connection)
    {
    # 空键:所有观察进入同一个全局统计。
    # 每次 connection_established 事件贡献 1。
    SumStats::observe("conn established",
                      SumStats::Key(),
                      SumStats::Observation($num=1));
    }

event zeek_init()
    {
    local r1 = SumStats::Reducer($stream="conn established",
                                $apply=set(SumStats::SUM));

    SumStats::create([$name = "counting connections",
                     $epoch = 1min,
                     $reducers = set(r1),
                     $epoch_result(ts: time,
                                   key: SumStats::Key,
                                   result: SumStats::Result) =
                        {
                        print fmt("Number of connections established: %.0f",
                                  result["conn established"]$sum);
                        }]);
    }

脚本开头加载 SumStats。每次 connection_established 事件发生,observe 都向 conn established 流投递数值 1。zeek_init 中创建的 Reducer 订阅完全相同的流名,用 SumStats::SUM 累加这些观察。

最终统计名可以自定,$epoch = 1min 则定义一分钟窗口。$epoch_result 接收窗口相关时间、键和结果表,读取对应流的 $sum。这个字段是 double,因此示例用 %.0f 输出无小数位的数值,而不是整数格式符。

要注意,脚本数的是 Zeek 产生的 connection_established 事件,不是原始数据包个数,也不是“所有 TCP/UDP 活动”的通用计数。观察流名拼错,或订阅了不同的事件,都可能得到与预期不同的结果。

官方文档在配套测试抓包上给出以下命令和结果。下面是上游预期输出,不是本文执行日志:

$ zeek -r workshop_2011_browse.trace sumstats-countconns.zeek
Number of connections established: 6

-r 指示 Zeek 读取已有抓包文件。想复核这个数字,需要同一个样本、相应版本及脚本;换一个 PCAP 后没有理由仍然得到 6。本文未下载或运行该样本。

示例二:按来源主机观察连接尝试

第二个例子演示阈值功能。它使用 connection_attempt 事件,把未成功的连接尝试按来源主机聚合。在五分钟窗口里,当累计值跨越 5.0 的阈值时打印提示。

这只是阈值教学示例,不能把它当作生产级扫描检测,更不能直接接入自动封禁。源文对此有明确限定。正常客户端遇到服务故障、路由问题或错误配置,也可能产生多次失败尝试;反之,采用不同节奏或成功建立连接的扫描也不一定被这个示例覆盖。

@load base/frameworks/sumstats

event connection_attempt(c: connection)
    {
    # 按尝试建立连接的来源主机分组。
    # 每次 connection_attempt 事件贡献 1。
    SumStats::observe("conn attempted",
                      SumStats::Key($host=c$id$orig_h),
                      SumStats::Observation($num=1));
    }

event zeek_init()
    {
    local r1 = SumStats::Reducer($stream="conn attempted",
                                $apply=set(SumStats::SUM));

    SumStats::create([$name = "finding scanners",
                     $epoch = 5min,
                     $reducers = set(r1),
                     $threshold = 5.0,
                     $threshold_val(key: SumStats::Key,
                                    result: SumStats::Result) =
                        {
                        return result["conn attempted"]$sum;
                        },
                     $threshold_crossed(key: SumStats::Key,
                                        result: SumStats::Result) =
                        {
                        print fmt("%s attempted %.0f or more connections",
                                  key$host, result["conn attempted"]$sum);
                        }]);
    }

与第一个例子相比,有三处本质变化。观察事件从已建立连接改为连接尝试;键从全局空键改为 c$id$orig_h,于是每个来源主机有自己的结果;输出从窗口结束回调改为阈值计算与跨越阈值回调。

$threshold_val 负责从结果中提取要比较的数值,这里直接返回求和结果。$threshold 提供阈值 5.0。$threshold_crossed 则描述达到触发条件后做什么:本文与上游都只打印文本,没有调用防火墙、执行命令或主动向目标发送探测。

上游脚本有一行沿用了第一例的“每个已建立连接计为 1”注释,但这里实际事件是 connection_attempt。本文把注释改为“每次连接尝试事件贡献 1”,没有改变程序行为。

官方给出的回放例子如下,同样只是文档列出的预期输出:

$ zeek -r nmap-vsn.trace sumstats-toy-scan.zeek
192.168.1.71 attempted 5 or more connections

这个 PCAP 中包含主机运行 nmap 的流量,因此该主机在样本中越过了演示阈值。192.168.1.71 是上游样本的输出,不是本稿探测出的设备。本文没有执行 nmap,也不需要主动扫描任何网络来理解这个例子。

从教学示例走向可维护统计

编辑补充:部署之前应先定义观察对象、键的基数、窗口长度、阈值的含义以及结果的消费者。全局空键只有一组状态;按来源地址分组则会随着来源数量增加而占用更多内存。缩短窗口可以改变状态保留周期,但不能取代对键数量和事件速率的实际观测。

在隔离测试目录回放有权使用的 PCAP,能减少对在线网络的干扰,但抓包本身可能包含敏感通信数据;Zeek 也可能在当前目录产生日志。应使用批准的样本并管理输出文件。本文只静态核对了接口调用、流名、键和回调,未测量内存占用、吞吐量、集群合并延迟或误报率。

若后续把结果接入告警,应另外设计抑制重复通知、解释阈值与保留证据的方式。这个示例没有给出生产场景的最佳窗口或最佳阈值,不应把 5 分钟和 5 次当成普遍成立的扫描判据。

来源、变更与许可

本文为未完纪中文编译,完整覆盖原章;修改包括中文解释、注释纠正与排版、自绘流程图,以及明确标注的运行与安全边界。全文及代码静态核验日期:2026-10-05;没有把上游预期输出声称为本地测试。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容