长亭百川云 - 文章详情

牧云插件系统技术选型之插件通信框架大PK

huraway

136

2023-05-19

本文将探讨在基于 WASM 的牧云主机安全系统的插件系统开发中通信方式的选择。我们将对比不同通信在性能、安全性、易用性等方面的优缺点,并解释我们为什么以及最终选择了何种通信方案。

这是系列文章“主机Agent插件引擎开发故事”的第六篇,后续将会持续更新。该系列文章将带领您深入探究长亭牧云团队主机Agent插件引擎的开发历程,内容涵盖技术选型、插件接口设计、组件通信框架等多个方面,并详细讲解背后的原理和实现方式,无论您是网络安全专业人员还是对技术开发感兴趣的读者,都可以从中得到收获。我们希望通过分享在开发过程中面临的挑战、解决方案以及实践经验,提供深入见解和有价值的技术参考,帮助读者了解如何构建高效可靠的安全产品,共同推动安全技术社区的发展。

引言

牧云主机安全系统是一个基于 WebAssembly 的安全解决方案,旨在提供可信赖的主机环境和保护用户数据的安全性。

插件系统是牧云主机安全系统的核心组成部分,它允许用户根据特定需求自定义功能,增加自己的扩展模块。通过插件系统,用户可以为主机环境添加新的安全策略、监控和审计功能,以满足不同应用场景的需求。

插件系统开发的目标是提供一个灵活、安全、高性能的插件框架,使用户能够轻松开发和集成插件。为了实现一个灵活高效的插件系统,开发团队需要选择合适的通信方式和 RPC 框架。

插件系统通信需求

涉及的通信类型

插件系统涉及的通信类型包括以下几个方面:

  1. 插件与宿主程序的通信:

插件需要与宿主程序进行交互,例如获取系统信息、调用宿主程序提供的功能接口等。

  1. 插件执行器间的通信:

插件执行器之间可能需要进行通信和协调工作,例如传递数据、共享资源、实现任务分配等。

  1. 插件执行器与核心引擎的通信:

核心引擎为插件提供安全功能所需的安全事件源。插件执行器需要与核心引擎进行交互,例如获取引擎的状态信息、发送指令、接收引擎的事件通知等。

  1. 核心引擎与管理器间的通信:

管理器为Agent程序整体提供管理职能。核心引擎需要与管理器进行通信以传递系统状态、接收管理器的指令、代理 IO 请求等。

  1. 管理器为其他组件提供的状态维持和协同控制信号:

管理器需要为其他组件提供状态维持和协同控制的信号,例如传递配置信息、启动/停止插件执行器等。

  1. 管理器与服务平台间的数据上报、配置下发和其他通知信号:

管理器需要与服务平台进行通信,例如向平台上报数据、接收配置信息、发送通知信号等。

这些通信需求涵盖了插件系统内部各个组件之间的交互和协作。为确保插件能够与宿主环境、执行器、引擎和管理器等进行有效的通信,以实现功能扩展、资源共享和协同工作的目标,必须选择合适的通信方式和协议,确保通信的高效性、安全性和可靠性。

插件通信框架中的消息特征

在牧云插件系统中,需要处理复杂的数据结构和具有特定特征的通信。以下是通信中的一些主要特征:

  1. 数据结构复杂性:

    • 通信中需要传递的数据结构通常是复杂的,包含多层嵌套和各种数据类型。这些数据结构可能包括对象、数组、枚举等,需要有效地操作序列化和反序列化。
  2. 大量短消息和大消息:

    • 通信过程中,主要以大量的短消息为主(即主机安全系统需要处理的各类安全事件),但也会出现一些较大的单条消息(包含较多上下文信息)。因此,通信框架需要能够高效地处理大量的小型消息,并支持传输和处理较大的消息。
  3. 异步通信:

    • 通信必须异步操作,以提高系统的响应性能和并发处理能力。否则无法做到高效响应大量的安全事件。
  4. 数据类型模型差异:

    • 主机 Agent 程序的不同组件涉及到大量的混合语言编程,而不同的编程语言具有不同的数据类型模型。因此通信框架需要能够处理不同数据类型之间的转换和兼容性,确保数据的正确传输和解析。

以上在选择通信框架时都需要考虑到,并确保选择的框架能够满足这些需求。

通信方式选型

说到跨组件的通信,大多数人首先想到的可能就是各种 RPC,然后就是各种网络通信。然而,这里我们想说的首先不一定要用 RPC,其次最好是不用网络栈。要考虑一个技术问题的最佳选型,首先要从业务需求出发,其次最重要的是从底层技术出发

底层通信方式的优缺点

下面列举了不同底层通信方式的优缺点:

从底层技术角度来看,值得考虑的通信方式包括:

  • FFI
    • ABI Stable
  • Share Memory
  • Dynamic Library Load
  • IPC
    • Unix Domain Socket
    • Pipe/FIFO on Unix
    • Named Pipe on Windows
    • STDIO
    • MMAP
  • Network
    • TCP

下面,我们来逐一分析这些通信方式的特点和适用场景:

  1. FFI (Foreign Function Interface):

    • ABI Stable: 使用稳定的应用程序二进制接口,允许不同编程语言之间直接调用函数。适用于需要高性能、低延迟的通信场景,但是相对来说调试困难、组件间耦合度高,容易出现接口变化和版本变化导致的兼容性问题。对于 Rust 来说,由于 Rust 本身的 ABI 接口尚未稳定,需要通过 C ABI 接口通信。
  2. 共享内存 (Shared Memory):

    • 使用共享内存实现进程间通信,可以实现高效的数据传输和共享。适用于需要高性能、低延迟的本地通信场景,但需要正确处理并发控制和同步问题,容易出现竞态条件和数据不一致的情况,较难实现正确的并发控制。
  3. 动态库加载 (Dynamic Library Load):

    • 加载动态库并调用其中的函数实现通信。灵活性较高,适用于需要动态扩展和灵活配置的通信场景,但容易导致运行时错误,需要确保加载的动态库版本和接口的一致性,还需要处理动态库的生命周期管理。
  4. 进程间通信 (Inter-Process Communication, IPC):

    • Unix Domain Socket: 在 Unix-like 系统中,提供高效、简单、直观、稳定的本地进程间通信方式。
    • Pipe / FIFO: 在 Unix 系统中,使用管道或 FIFO(命名管道)提供了一种简单的进程间通信方式,但仅支持单向字节流。适用于共享继承关系的进程间通信场景。
    • Named Pipe : 在 Windows 系统中,称作命名管道的技术更加强大,提供了一种简单、支持全双工的通信方式,而且可以支持通过网络通信实现跨机器的进程间通信。
    • STDIO: 使用标准输入输出流进行进程间通信是一种简单方式,具有所有的平台支持,但这是一种同步的阻塞 IO,相对较慢,且仅适用于父子进程通信场景。
  5. 内存映射 (Memory Mapping, MMAP):

    • 使用内存映射技术将文件映射到进程的地址空间,若共同映射同一个文件,可实现进程间共享数据,但实现正确的并发读写和同步控制较难,且容易暴露敏感数据。适用于需要高效的数据读写和同步控制的通信场景。
  6. 网络通信 (Network):

    • TCP: 使用传输控制协议进行网络通信。跨平台和跨网络的通信方式,成熟稳定,但相对较慢。

在选择通信方式时,需根据具体业务需求和底层技术来进行权衡。需要考虑通信的性能要求、跨平台和跨网络的需求、安全性要求以及灵活性和易用性等因素。

此外,还需要考虑通信协议的支持和适配性,以确保通信方式与插件系统的上层应用层协议相匹配,实现数据的流式传输、流压缩、安全加密等特性。

选择原则

在选择通信方式时,通常可以考虑以下原则:

  1. 性能和效率:选择通信方式应具备高性能和高效率,能够满足系统对实时性和吞吐量的要求。

  2. 安全性和稳定性:选择通信方式应具备一定的安全性保障,能够保护通信过程中的数据传输和访问安全,同时具备稳定性,减少通信故障的发生。

  3. 可扩展性和灵活性:选择通信方式应具备良好的可扩展性,能够适应系统的需求变化和功能扩展,并且灵活性高,易于开发人员使用和维护。

  4. 跨平台和跨机器支持:选择通信方式应能够在不同操作系统和机器之间进行通信,以提供良好的跨平台和分布式环境的支持。

  5. 开发和维护成本:选择通信方式时需要考虑开发和维护的成本,包括学习成本、调试和故障排查成本,以及后续扩展和维护的成本。

  6. 社区支持和生态系统:选择通信方式时可以考虑其是否有活跃的社区支持和完善的生态系统,能够提供开发文档、示例代码和解决方案等资源。

  7. 上层应用兼容性:选择通信方式时需要考虑上层应用的兼容性,确保通信方式能够与上层应用的协议和接口相匹配,实现数据的正确传输和解析。

特定于主机安全的场景来说,我们还需要特别注意简单、高效、异步、容易正确实现、支持复杂通信、需要同时考虑不同平台这几点。

选择结果

基于之前提到的选择原则,以下是我们的选择结果:

  1. 本地通信:

    • Linux 下,我们选择使用 Unix Domain Socket 作为本地通信方式。它具有高效、稳定、仅限于本机使用的特点。
    • Windows 下,我们选择使用 Named Pipe 作为本地通信方式。它支持全双工通信。
  2. 跨机器通信:

    • 我们选择基于 TCP 的方式作为跨机器通信方式。TCP 具有跨平台和稳定性的优势,并且适用于分布式环境。

应用层通信协议选型

适合的底层通信方式为后续的通信协议和上层应用提供了可靠的基础。但在底层通信机制的基础上,还需要更加便捷的上层通信协议来提供包括流压缩、多路复用、安全加密等特性,值得考虑的通信方式包括:

  1. OpenAPI on HTTP 2.0 over TLS

    • 优点:标准化的RESTful API描述方式,支持流压缩、安全加密等特性。
    • 限制:相对于其他选项,可能在性能上稍逊一筹。
  2. gRPC on HTTP 2.0 over TLS

    • 优点:高效的数据传输机制,支持流式传输、多路复用和安全加密。
    • 限制:对于本地通信来说,可能过于复杂重量级。
  3. WebSocket over TLS

    • 优点:支持全双工通信,适用于实时或需要频繁交互的应用场景。
    • 限制:可能与 Raw WebSocket 相比增加了一些计算开销。
  4. Raw WebSocket over Unix Domain Socket / Named Pipe on Windows

    • 优点:支持全双工通信,与 Unix Domain SocketNamed Pipe 相结合,无需网络栈的参与,降低了通信的开销,无加密可以提升性能。通过结合 Raw WebSocketOS 级别的 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 框架

有了基础的通信模型,我们还需要有具体的通信方式,这里我们需要选择一种 RPC 框架。考虑以下方法:

gRPC/tarpc 和其他所有设计目标为微服务设计的 RPC 协议对于插件本地环境访问来说都太复杂太重了。

JSON-RPC 无状态,结构简单,相对比较适合,但是 JSON 的序列化反序列化太慢/内存消耗太高/数据类型模型与 Rust 差别较大,可以采用 bincode 替代 JSON-RPC 中的序列化方法。

tarpcRust 实现的采用 bincode 作为序列化方法的 RPC 协议,但 tarpc 为微服务场景设计,不必要的开销较大,不适合直接采用。

综上,自行根据 JSON-RPC 的协议设计实现一个 bincode-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 SocketNamed PipeTCP。针对不同的通信需求,我们选择了合适的通信方式。

在应用层通信协议的选型中,我们考虑了 OpenAPIgRPCWebSocketRaw WebSocket 等选项,并根据需求选择了适合的协议。

针对 RPC 框架的选型,我们综合了现有框架的优缺点,并最终决定自行实现 bincode-rpc 框架。bincode-rpc 具有纯Rust 实现、性能可控、易用性强、支持异步等特点,适合频繁小数据和本机可靠通信的需求。

通过上述选择,我们为基于 WASM 的牧云主机安全系统的插件系统开发提供了一个高效、安全、可靠的通信方案。插件系统能够实现与宿主程序、执行器、引擎和管理器之间的无缝通信和协同工作,满足用户的定制需求,提升系统的灵活性和功能扩展性。

相关博文推荐

上一篇:【牧云插件系统技术选型之插件开发用什么?】

系列文章目录:【预告】主机Agent插件引擎开发故事汇总

  1. 【揭秘牧云插件开发者的创新之路:从无法解决的问题到“妙趣横生”】
  2. 【牧云插件系统面向未来的设计原则】
  3. 【牧云插件系统选型斗争】
  4. 【牧云插件系统技术选型之探针开发用什么?】
  5. 【牧云插件系统技术选型之插件开发用什么?】
  6. 【牧云插件系统技术选型之插件通信框架大PK】
  7. 【牧云插件系统技术选型之自己人都会吐槽的序列化方法选择之争】
  8. 【牧云插件系统技术选型之WASM运行时选哪个?不选这个竟然出大问题!】
相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2