☰
Zeek 远程控制框架(Control Framework)实战:基于 Broker 的运行时命令通道解析
2026/10/7 9:55:14 网站建设 项目流程
  • 网络安全
  • 网络
  • 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
点击查看免费下载

base/frameworks/control是 Zeek 内置的远程控制框架,它借助 Broker 消息总线,为运行中的 Zeek 实例提供了一套"命令"机制:既可以在运行时远程修改实例的配置(如重定义&redef常量),也可以远程收集实例的运行信息(变量值、对端通信状态、网络统计等)。阅读本文后,你将掌握控制端(controller)与被控端(controllee)的完整运行模型、id_value/peer_status/net_stats/configuration_update/shutdown五种内建命令的用法,并能结合仓库源码与 btest 测试用例,在自己的 Zeek 环境中搭建可用的远程控制通道。

框架定位:为"运行时远程干预"提供基础

Zeek 官方的框架文档对它的定位非常明确:The control framework provides the foundation for providing "commands" that can be taken remotely at runtime to modify a running Zeek instance or collect information from the running instance.(见 doc/scripts/base/frameworks/control/index.rst)。也就是说,这个框架解决的核心问题不是入侵检测本身,而是运维侧的可控性——让一个已经跑起来的 Zeek 进程,可以被外部通过消息通道"指挥"。

它依赖两个关键基础设施:

  1. Broker 消息总线:所有控制指令和应答都以 Broker 事件的形式在节点之间交换;
  2. 集群(Cluster)通信节点配置:控制消息的路由目标(Cluster::node)与发布(Cluster::publish)均建立在集群命名空间之上。

从代码结构看,框架被拆成了三层:

  • 基础层:scripts/base/frameworks/control/main.zeek——定义模块、常量、命令集与全部事件签名;
  • 被控端实现:scripts/policy/frameworks/control/controllee.zeek——加载到"被控制"的 Zeek 实例上,负责应答命令;
  • 控制端实现:scripts/policy/frameworks/control/controller.zeek——一个一次性命令行工具,向远端发出命令并退出。

__load__.zeek只是简单地@load ./main(见 scripts/base/frameworks/control/load.zeek),因此直接加载基础框架并不会引入任何收发逻辑,真正的行为完全由后两个策略脚本决定。

核心模块 main.zeek:命令集与事件契约

scripts/base/frameworks/control/main.zeek 定义了module Control下的全部"合同",是理解整个框架的起点。

通信配置常量

常量默认值含义
Control::topic_prefix"zeek/control"所有控制消息在 Broker 上使用的主题前缀
Control::controllee_listenT(可 redef)被控端是否自行调用Broker::listen();集群模式下由集群初始化流程统一调用,无需重复监听
Control::host0.0.0.0(可 redef)被控主机的地址
Control::host_port0/tcp(可 redef)被控主机的端口
Control::zone_id""(可 redef)当Control::host是非全局 IPv6 地址、需要指定 RFC 4007 的zone_id时使用
Control::cmd""(可 redef)要执行的命令,通常在命令行上通过Control::cmd=...指定
Control::arg""(可 redef)需要参数的命令(如id_value)所携带的参数

内建命令集

const commands: set[string] = { "id_value", "peer_status", "net_stats", "configuration_update", "shutdown", } &redef;

五种命令分别对应三类能力:读取信息(id_value、peer_status、net_stats)、修改配置(configuration_update)、生命周期管理(shutdown)。另有一个Control::ignore_ids: set[string] = { }常量,用于在配置同步时排除特定变量 ID。

事件对:请求 / 响应

框架采用"请求-响应"事件对设计,每个命令都有一对事件:

请求事件响应事件用途
Control::id_value_request(id: string)Control::id_value_response(id: string, val: string)请求/返回某个变量 ID 的值
Control::peer_status_request()Control::peer_status_response(s: string)请求/返回当前通信状态
Control::net_stats_request()Control::net_stats_response(s: string)请求/返回网络统计
Control::configuration_update_request()Control::configuration_update_response()请求/应答配置更新;Control::configuration_update是它的别名事件,也是框架的主要挂接点
Control::shutdown_request()Control::shutdown_response()请求/应答关闭实例

main.zeek末尾还定义了一个辅助事件:

event terminate_event() &is_used { terminate(); }

它把"结束进程"封装成可调度的事件,被控端在收到shutdown_request后会延迟调度它,控制端则在收到任何响应后调用它退出。

被控端 controllee.zeek:如何让一个 Zeek 实例可被远程控制

要让某个 Zeek 实例接受远程控制,只需在启动参数中追加控制端脚本(见 scripts/policy/frameworks/control/controllee.zeek 头部注释):

zeek <scripts> frameworks/control/controllee

<scripts>是你想在该实例上运行的分析脚本集。同时,通信节点配置中需要有一个节点被配置为 controller 节点。

启动时的订阅与监听

event zeek_init() &priority=-10 { if ( Cluster::backend == Cluster::CLUSTER_BACKEND_BROKER ) { Broker::subscribe(Control::topic_prefix + "/" + Broker::node_id()); if ( Control::controllee_listen ) Broker::listen(); } }

在zeek_init(优先级-10)时,被控端订阅zeek/control/<自身 node_id>主题——这是它接收指令的"信箱地址";若Control::controllee_listen为真则主动监听端口。注意集群后端必须是 Broker(CLUSTER_BACKEND_BROKER),且集群模式下初始化流程已经统一处理监听,因此controllee_listen在该场景下无需为真。

四种信息类命令的应答实现

id_value_request的应答最直观地展示了"远程读取变量"的机制:

event Control::id_value_request(id: string) { local val = lookup_ID(id); local reply_topic = Control::topic_prefix + "/id_value_response/" + Cluster::node; Cluster::publish(reply_topic, Control::id_value_response, id, fmt("%s", val)); }

它调用 Zeek 解释器内置的lookup_ID()在当前全局作用域中查找变量 ID 并格式化其值,然后发布到zeek/control/id_value_response/<cluster node>主题。lookup_ID是核心解释器的公开接口,定义于 src/Scope.cc(const IDPtr& lookup_ID(const char* name, ...)),与脚本层事件处理一一对应。

peer_status_request遍历Broker::peers(),按行输出每个对端的网络时间、peer ID、地址与状态:

status += fmt("%.6f peer=%s host=%s status=%s\n", network_time(), bpeer$peer$id, bpeer$peer$network$address, bpeer$status);

net_stats_request调用get_net_stats()获取收包统计,输出recvd、dropped、link三个计数:

local reply = fmt("%.6f recvd=%d dropped=%d link=%d\n", network_time(), ns$pkts_recvd, ns$pkts_dropped, ns$pkts_link);

两个响应都发布到zeek/control/<cmd>_response/<Cluster::node>主题。

配置更新与关停

configuration_update_request的处理很关键——它并不自己做实质更新,而是触发别名事件并通知各脚本:

event Control::configuration_update_request() { event Control::configuration_update(); # ... 由其他脚本处理 redef-able const 的运行时修改 ... local topic = Control::topic_prefix + "/configuration_update_response/" + Cluster::node; Cluster::publish(topic, Control::configuration_update_response); }

shutdown_request先回发确认事件,再延迟 1 秒调度terminate_event():

schedule 1sec { terminate_event() };

延迟的目的在注释中写得很清楚:let the current event queue flush itself first,即先让当前事件队列排空,避免在事件处理中途强行终止导致丢失数据。

控制端 controller.zeek:一次性的远程命令客户端

scripts/policy/frameworks/control/controller.zeek 被设计为一个"发完命令即退出"的实用脚本,官方用法(见文件头部注释):

zeek <scripts> frameworks/control/controller \ Control::host=<host_addr> Control::host_port=<host_port> \ Control::cmd=<command> [Control::arg=<arg>]

启动时的参数校验与建连

event zeek_init() &priority=5 { if ( cmd !in commands ) { Reporter::error(fmt("The '%s' control command is unknown.", cmd)); terminate(); } Broker::subscribe(Control::topic_prefix); Broker::peer(cat(host), host_port); }

控制端在zeek_init(优先级5)先校验Control::cmd是否属于内建命令集,非法命令直接报错退出;随后订阅整个zeek/control主题(接收所有节点发回的响应),并调用Broker::peer(cat(host), host_port)与被控端建立对等连接。

命令派发:send_control_request

send_control_request()根据cmd将请求发布到指定主题:

case "id_value": if ( arg == "" ) Reporter::fatal("The Control::id_value command requires that Control::arg also has some value."); Broker::publish(topic, Control::id_value_request, arg); break; case "peer_status": Broker::publish(topic, Control::peer_status_request); break; case "net_stats": Broker::publish(topic, Control::net_stats_request); break; case "shutdown": Broker::publish(topic, Control::shutdown_request); break; case "configuration_update": Broker::publish(topic, Control::configuration_update_request); break;

可以看到id_value是唯一必须携带Control::arg的命令,缺参时直接Reporter::fatal终止。

建连回调:peer_added 与配置同步

命令实际是在Broker::peer_added(优先级-10)中触发的,因为此时才确认到与被控端的连接已建立:

event Broker::peer_added(endpoint: Broker::EndpointInfo, msg: string) &priority=-10 { local topic = Control::topic_prefix + "/" + endpoint$id; if ( cmd == "configuration_update" ) { if ( ! Broker::enable_identifier_updates ) Reporter::fatal("Identifier updates are disabled."); local ids = configurable_ids(); for ( id in ids ) if ( Broker::publish_id(topic, id) ) ++publish_count; Reporter::info(fmt("Control framework sent %d IDs", publish_count)); } send_control_request(topic); }

configuration_update走的是 Broker 的**标识符更新(identifier update)**机制:控制端通过Broker::publish_id()把被控端所有"可配置"的 ID 推送过去。因此,使用configuration_update前必须在被控端开启Broker::enable_identifier_updates(默认关闭),否则控制端会直接报错退出。

configurable_ids()函数给出了"可配置 ID"的精确筛选规则:

# Skip it if the variable isn't redefinable or not const. if ( t$constant && t$redefinable && t$type_name != "func" ) rval[id] = t;

即只有&redef声明的const全局变量才会被同步;普通全局变量(通常是状态存储)被刻意排除,注释解释了原因:those values will frequently be declared with &redef so that attributes can be redefined;函数类型因序列化支持不完整而暂不发送。被列入Control::ignore_ids的 ID 也会被跳过。

控制端收到对应响应事件后(各响应 handler 均以优先级-10挂接),统一调用terminate_event()退出,完成"发出命令 → 收到应答 → 进程结束"的完整生命周期。

五种内建命令速查与实战组合

综合main.zeek的事件定义与控制/被控端实现,五种命令可总结为:

命令需要参数数据流向典型用途
id_valueControl::arg=<变量 ID>控制端 → 被控端 → 控制端远程读取任意全局变量的当前值
peer_status无同上查看被控端当前的 Broker 对端连接状态
net_stats无同上查看收包 / 丢包 / 链路统计
configuration_update无控制端 → 被控端(推送全部可 redef 常量)运行时热更新配置
shutdown无控制端 → 被控端(回发确认)优雅关停远端实例

一个读取变量值的完整调用示例:

zeek frameworks/control/controller \ Control::host=127.0.0.1 Control::host_port=5000/tcp \ Control::cmd=id_value Control::arg=test_var

对应的被控端(假定运行在127.0.0.1:5000)会执行lookup_ID("test_var"),并把结果通过Control::id_value_response发布回控制端打印。

测试用例佐证:btest 中的端到端验证

仓库自带的 btest 测试直接给出了可复现的最小环境,是理解命令真实调用链的最佳范本(均位于 testing/btest/scripts/base/frameworks/control/):

  • id_value.zeek:后台同时启动controllee与controller两个进程,控制器以Control::cmd=id_value Control::arg=test_var运行。测试特意在被控端用redef test_var = "This is the value from the controllee"覆盖原值,并在控制端打印id_value_response,验证远程读取到的是被控端实例上的值,而非控制端自身脚本的值。
  • configuration_update.zeek:被控端先redef Broker::enable_identifier_updates = T;,控制端加载额外的test-redef.zeek脚本并以Control::cmd=configuration_update连接。被控端在zeek_init与zeek_done各打印一次test_var,通过两次输出的差异验证运行时配置同步确实生效。
  • shutdown.zeek:控制端以Control::cmd=shutdown运行,验证被控端能收到请求、回发shutdown_response并延迟终止。

这些测试还揭示了一个易被忽略的部署细节:id_value测试因涉及"多个 Zeek 进程同时运行"而不适用于-O gen-C++编译优化模式(见测试文件头部@TEST-REQUIRES声明),实战中若启用 ZAM/gen-C++ 编译,应确认控制场景仍按预期工作。

注意事项与适用边界

  • Broker 是硬依赖:被控端仅在Cluster::backend == Cluster::CLUSTER_BACKEND_BROKER时执行订阅/监听逻辑,因此该框架面向 Broker 后端集群或单机 Broker 环境。
  • 集群模式差异:集群中由 setup 流程统一调用Broker::listen(),故Control::controllee_listen通常无需为真;单机手动控制场景则保留默认T。
  • 配置同步的前提条件:configuration_update依赖 Broker 标识符更新特性,被控端必须显式redef Broker::enable_identifier_updates = T;,且只会同步&redef的const变量,函数与ignore_ids中的 ID 不参与。
  • 安全边界:控制消息通道本身不做鉴权设计,shutdown可远程终止实例、configuration_update可改写运行配置,因此该框架的暴露范围(监听地址、端口)应纳入运维安全考量,避免向不可信网络开放。

从 scripts/base/frameworks/control/main.zeek 的常量定义到 scripts/policy/frameworks/control/controller.zeek 的命令派发,再到 testing/btest/scripts/base/frameworks/control/ 的端到端测试,控制框架的每一条链路都能在仓库中找到可验证的落点。把它作为基础层,上层脚本可以自由挂接Control::configuration_update事件实现自定义的热更新逻辑,也可以扩展commands集合加入自己的远程命令,这正是 Zeek 把"远程控制"做成框架而非内置命令的原因。

  • 网络安全
  • 网络
  • 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
点击查看免费下载
上一篇:League-Toolkit 从零上手:这款英雄联盟客户端自动化工具箱到底能帮你做什么
下一篇:英雄联盟Akari助手:一次对局准备从 30 秒到 3 秒的游戏效率工具

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

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

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

立即咨询