本文将探讨在基于 WASM 的牧云主机安全系统的插件系统开发中通信方式的选择。我们将对比不同通信在性能、安全性、易用性等方面的优缺点,并解释我们为什么以及最终选择了何种通信方案。
这是系列文章“主机Agent插件引擎开发故事”的第六篇,后续将会持续更新。该系列文章将带领您深入探究长亭牧云团队主机Agent插件引擎的开发历程,内容涵盖技术选型、插件接口设计、组件通信框架等多个方面,并详细讲解背后的原理和实现方式,无论您是网络安全专业人员还是对技术开发感兴趣的读者,都可以从中得到收获。我们希望通过分享在开发过程中面临的挑战、解决方案以及实践经验,提供深入见解和有价值的技术参考,帮助读者了解如何构建高效可靠的安全产品,共同推动安全技术社区的发展。
牧云主机安全系统是一个基于 WebAssembly
的安全解决方案,旨在提供可信赖的主机环境和保护用户数据的安全性。
插件系统是牧云主机安全系统的核心组成部分,它允许用户根据特定需求自定义功能,增加自己的扩展模块。通过插件系统,用户可以为主机环境添加新的安全策略、监控和审计功能,以满足不同应用场景的需求。
插件系统开发的目标是提供一个灵活、安全、高性能的插件框架,使用户能够轻松开发和集成插件。为了实现一个灵活高效的插件系统,开发团队需要选择合适的通信方式和 RPC 框架。
插件系统涉及的通信类型包括以下几个方面:
插件需要与宿主程序进行交互,例如获取系统信息、调用宿主程序提供的功能接口等。
插件执行器之间可能需要进行通信和协调工作,例如传递数据、共享资源、实现任务分配等。
核心引擎为插件提供安全功能所需的安全事件源。插件执行器需要与核心引擎进行交互,例如获取引擎的状态信息、发送指令、接收引擎的事件通知等。
管理器为Agent程序整体提供管理职能。核心引擎需要与管理器进行通信以传递系统状态、接收管理器的指令、代理 IO
请求等。
管理器需要为其他组件提供状态维持和协同控制的信号,例如传递配置信息、启动/停止插件执行器等。
管理器需要与服务平台进行通信,例如向平台上报数据、接收配置信息、发送通知信号等。
这些通信需求涵盖了插件系统内部各个组件之间的交互和协作。为确保插件能够与宿主环境、执行器、引擎和管理器等进行有效的通信,以实现功能扩展、资源共享和协同工作的目标,必须选择合适的通信方式和协议,确保通信的高效性、安全性和可靠性。
在牧云插件系统中,需要处理复杂的数据结构和具有特定特征的通信。以下是通信中的一些主要特征:
数据结构复杂性:
大量短消息和大消息:
异步通信:
数据类型模型差异:
Agent
程序的不同组件涉及到大量的混合语言编程,而不同的编程语言具有不同的数据类型模型。因此通信框架需要能够处理不同数据类型之间的转换和兼容性,确保数据的正确传输和解析。以上在选择通信框架时都需要考虑到,并确保选择的框架能够满足这些需求。
说到跨组件的通信,大多数人首先想到的可能就是各种 RPC
,然后就是各种网络通信。然而,这里我们想说的首先不一定要用 RPC
,其次最好是不用网络栈。要考虑一个技术问题的最佳选型,首先要从业务需求出发,其次最重要的是从底层技术出发。
下面列举了不同底层通信方式的优缺点:
从底层技术角度来看,值得考虑的通信方式包括:
下面,我们来逐一分析这些通信方式的特点和适用场景:
FFI (Foreign Function Interface):
Rust
来说,由于 Rust
本身的 ABI
接口尚未稳定,需要通过 C ABI
接口通信。共享内存 (Shared Memory):
动态库加载 (Dynamic Library Load):
进程间通信 (Inter-Process Communication, IPC):
Unix Domain Socket
: 在 Unix-like
系统中,提供高效、简单、直观、稳定的本地进程间通信方式。Pipe
/ FIFO
: 在 Unix
系统中,使用管道或 FIFO
(命名管道)提供了一种简单的进程间通信方式,但仅支持单向字节流。适用于共享继承关系的进程间通信场景。Named Pipe
: 在 Windows
系统中,称作命名管道的技术更加强大,提供了一种简单、支持全双工的通信方式,而且可以支持通过网络通信实现跨机器的进程间通信。STDIO
: 使用标准输入输出流进行进程间通信是一种简单方式,具有所有的平台支持,但这是一种同步的阻塞 IO
,相对较慢,且仅适用于父子进程通信场景。内存映射 (Memory Mapping, MMAP):
网络通信 (Network):
TCP
: 使用传输控制协议进行网络通信。跨平台和跨网络的通信方式,成熟稳定,但相对较慢。在选择通信方式时,需根据具体业务需求和底层技术来进行权衡。需要考虑通信的性能要求、跨平台和跨网络的需求、安全性要求以及灵活性和易用性等因素。
此外,还需要考虑通信协议的支持和适配性,以确保通信方式与插件系统的上层应用层协议相匹配,实现数据的流式传输、流压缩、安全加密等特性。
在选择通信方式时,通常可以考虑以下原则:
性能和效率:选择通信方式应具备高性能和高效率,能够满足系统对实时性和吞吐量的要求。
安全性和稳定性:选择通信方式应具备一定的安全性保障,能够保护通信过程中的数据传输和访问安全,同时具备稳定性,减少通信故障的发生。
可扩展性和灵活性:选择通信方式应具备良好的可扩展性,能够适应系统的需求变化和功能扩展,并且灵活性高,易于开发人员使用和维护。
跨平台和跨机器支持:选择通信方式应能够在不同操作系统和机器之间进行通信,以提供良好的跨平台和分布式环境的支持。
开发和维护成本:选择通信方式时需要考虑开发和维护的成本,包括学习成本、调试和故障排查成本,以及后续扩展和维护的成本。
社区支持和生态系统:选择通信方式时可以考虑其是否有活跃的社区支持和完善的生态系统,能够提供开发文档、示例代码和解决方案等资源。
上层应用兼容性:选择通信方式时需要考虑上层应用的兼容性,确保通信方式能够与上层应用的协议和接口相匹配,实现数据的正确传输和解析。
特定于主机安全的场景来说,我们还需要特别注意简单、高效、异步、容易正确实现、支持复杂通信、需要同时考虑不同平台这几点。
基于之前提到的选择原则,以下是我们的选择结果:
本地通信:
Linux
下,我们选择使用 Unix Domain Socket
作为本地通信方式。它具有高效、稳定、仅限于本机使用的特点。Windows
下,我们选择使用 Named Pipe
作为本地通信方式。它支持全双工通信。跨机器通信:
TCP
的方式作为跨机器通信方式。TCP
具有跨平台和稳定性的优势,并且适用于分布式环境。适合的底层通信方式为后续的通信协议和上层应用提供了可靠的基础。但在底层通信机制的基础上,还需要更加便捷的上层通信协议来提供包括流压缩、多路复用、安全加密等特性,值得考虑的通信方式包括:
OpenAPI on HTTP 2.0 over TLS
gRPC on HTTP 2.0 over TLS
WebSocket over TLS
Raw WebSocket
相比增加了一些计算开销。Raw WebSocket over Unix Domain Socket / Named Pipe on Windows
Unix Domain Socket
或 Named Pipe
相结合,无需网络栈的参与,降低了通信的开销,无加密可以提升性能。通过结合 Raw WebSocket
和 OS
级别的 IPC
机制可提供良好的平衡。对主机 Agent
来说,我们希望本地通信尽可能高效、多路、异步、不需要考虑加密;而远程通信要除了高效外需要更多考虑稳定、跨平台和安全特性等方面。
最终,对于本地通信,选择了在 Linux
系统上使用 Raw WebSocket over Unix Domain Socket
,在 Windows
系统上使用 Raw WebSocket over Named Pipe
,以实现高效、多路复用、异步的通信;对于远程通信,选择使用 gRPC on HTTP 2.0 over TLS
,以提供跨语言的交互性和稳定性。
在上层的具体业务中,有了应用层通信协议还不够,我们还需要在应用层通信协议的支持下使用更加贴近业务的通信模型管理组件之间的通信。具体来说,此处我们采用了中心辅助的点对点总线(Center-Assisted Peer-to-Peer Bus Communication)通信模型。各组件通过与注册中心通信来得知其需通信的另一个组件的地址和状态,通信过程在实际上是点对点的,而逻辑上则是通过总线的统一模型。
有了基础的通信模型,我们还需要有具体的通信方式,这里我们需要选择一种 RPC
框架。考虑以下方法:
gRPC
/tarpc
和其他所有设计目标为微服务设计的 RPC
协议对于插件本地环境访问来说都太复杂太重了。
JSON-RPC
无状态,结构简单,相对比较适合,但是 JSON 的序列化反序列化太慢/内存消耗太高/数据类型模型与 Rust 差别较大,可以采用 bincode
替代 JSON-RPC
中的序列化方法。
tarpc
是 Rust
实现的采用 bincode
作为序列化方法的 RPC
协议,但 tarpc
为微服务场景设计,不必要的开销较大,不适合直接采用。
综上,自行根据 JSON-RPC
的协议设计实现一个 bincode-rpc
是比较合适的方法。
考虑到使用场景为 频繁小数据
+ 本机可靠通信
,我们需要一种极度轻量/快速/简单/安全的 RPC 框架,任何现有的 RPC 框架都带来了过多不必要的消耗。因此需要自行实现 bincode-rpc
框架,设计基本遵循 JSON-RPC 2.0 SPEC。
bincode-rpc
的主要特性包括:
Rust
实现,性能可控且与探针语言一致proto
文件,schema
直接定义在代码中focus on ease of use
tokio
的异步下面是一个简单的 bincode-rpc
结构示例,包括请求结构和响应结构:
请求结构示例:
1{"bincoderpc": 1, "method": "subtract", "params": [42, 23], "id": 1}
1use bincode::{config, Decode, Encode};
2
3#[derive(Encode, Decode, PartialEq, Debug)]
4struct BincodeRpcRequest {
5 bincoderpc: u8,
6 method: String,
7 params: Vec,
8 id: u32,
9}
响应结构示例:
1{"bincoderpc": 1, "result": 19, "id": 1}
1use bincode::{config, Decode, Encode};
2
3#[derive(Encode, Decode, PartialEq, Debug)]
4struct BincodeRpcResponse {
5 bincoderpc: u8,
6 result: Vec,
7 id: u32,
8}
说明:
bincoderpc
字段用于标识协议版本号,固定为一个 u8
类型的值。method
字段表示远程调用的方法名,类型为 String
。params
字段是传递给方法的参数,类型为 Vec
,是实际参数结构体的序列化结果。id
字段用于标识请求和响应的对应关系,类型为 u32
。result
字段表示方法的返回结果,类型为 Vec
,是实际响应结果结构体的序列化结果。bincode-rpc
框架可以根据这样的结构定义进行序列化和反序列化,实现请求和响应的传输和解析。
通过本文的讨论,我们探讨了牧云主机安全系统的插件系统开发中的通信需求和技术选型。在通信方式选型中,我们考虑了底层技术的优缺点,并选择了适合的方式,即 Unix Domain Socket
、Named Pipe
和 TCP
。针对不同的通信需求,我们选择了合适的通信方式。
在应用层通信协议的选型中,我们考虑了 OpenAPI
、gRPC
、WebSocket
和 Raw WebSocket
等选项,并根据需求选择了适合的协议。
针对 RPC 框架的选型,我们综合了现有框架的优缺点,并最终决定自行实现 bincode-rpc
框架。bincode-rpc
具有纯Rust
实现、性能可控、易用性强、支持异步等特点,适合频繁小数据和本机可靠通信的需求。
通过上述选择,我们为基于 WASM
的牧云主机安全系统的插件系统开发提供了一个高效、安全、可靠的通信方案。插件系统能够实现与宿主程序、执行器、引擎和管理器之间的无缝通信和协同工作,满足用户的定制需求,提升系统的灵活性和功能扩展性。
系列文章目录:【预告】主机Agent插件引擎开发故事汇总