本文将探讨在基于WASM的牧云插件系统开发中插件开发语言的选择。我们将对比不同语言在性能、安全性、易用性等方面的优缺点,并解释我们为什么以及最终选择了哪种语言作为插件开发语言。
这是系列文章“主机Agent插件引擎开发故事”的第五篇,后续将会持续更新。该系列文章将带领您深入探究长亭牧云团队主机Agent插件引擎的开发历程,内容涵盖技术选型、插件接口设计、组件通信框架等多个方面,并详细讲解背后的原理和实现方式,无论您是网络安全专业人员还是对技术开发感兴趣的读者,都可以从中得到收获。我们希望通过分享在开发过程中面临的挑战、解决方案以及实践经验,提供深入见解和有价值的技术参考,帮助读者了解如何构建高效可靠的安全产品,共同推动安全技术社区的发展。
牧云插件系统是一个基于 WebAssembly
的高性能、跨平台、强隔离的主机安全系统。在设计和开发过程中,选择合适的插件开发语言对于确保插件的性能、安全性和易用性至关重要。选对编程语言能有效提高开发效率,降低维护成本,同时实现对多种语言的支持以适应未来发展。因此,在开发牧云插件系统时,我们需要深入探讨和权衡各种编程语言的优缺点,以找到最适合我们需求的解决方案。
总结一下,主机安全插件需要做到高性能、跨平台、强隔离、强类型、多语言、战未来。而这些正是 WebAssembly
的优点。
WebAssembly
顾名思义是一种 Web
的汇编语言,是一种可移植的编译格式。WebAssembly
的设计旨在提供更小的文件尺寸、更快的加载速度,在提供接近原生的执行的速度的同时提供高度的隔离性和安全性,旨在成为高级语言的编译目标。目前 WebAssembly
已经广泛应用于云函数、Serverless、客户端跨平台、容器技术等多种领域。我们则正在将其带到网络安全领域中。
类似于多种语言都可以编译为 JVM
的字节码,目前 C
、C++
、Rust
、Go
、Java
、C#
等许多语言都可以编译到 WebAssembly
字节码。
WebAssembly
本身只支持四个数据类型 s32
, s64
, u32
, u64
(即 32 位整数,64 位整数,32 位浮点和 64 位浮点)在模块(Guest
)和主机(Host
)之间通信。传统解决办法主要是手动传递指针并通过反射等手段指定指针指向的主机内存的使用方式,以此实现对复杂类型的支持。
(甚至字符串也算得上是复杂类型,只能通过这种方式间接使用。可见 WebAssembly
是一个多么 Low-Level
的计算模型。)
现在有两个困难的选择题摆在我们面前:
第一个是,有这么多语言可以选,选哪个。
第二个是,通信如此不便捷,用哪种方式通信。
关于第一个问题,Awesome WebAssembly Languages给出了一个选择的方向。正如所有其他 Awesome
类的 Repo
一样,其中包含了一个比较完备的可以编译到 WebAssembly
或者在WebAssembly
上有自己的 VM
的语言列表。
其中列出的达到 Stable
级别的语言包括以下这些(去掉虽然稳定但是不实用的,比如 Brainfuck
):
除了上面提到的 Stable
语言列表,值得考虑的语言还包括:
作为一个主机安全系统的插件引擎,我们不想要的东西包括:
Overhead
,包括虚拟机嵌套等
C#
/F#
/QuickJS
/Lua
/Python
C#
/F#
/Zig
/Rust
WASI
支持(不可接受)
C#
/F#
C
/C++
/Lua
C
/Lua
/Python
AssemblyScript
/QuickJS
/Lua
/Python
C#
/F#
/Python
C#
/F#
/Python
C#
/F#
/Python
所以我们值得考虑的选择还剩下:
关于 C
/C++
,在这种使用场景下没有得到其主要的好处却不得不承受其固有的缺点,所以排除。
关于 Go
/TinyGo
,是我们熟悉的语言,语法简单,具备一定的模块化和包管理特性,具备良好异步支持,没有虚拟机,是编写插件比较好的选择。考虑到 TinyGo
的编译产物文件大小具有显著优势,而缺失的特性可以主要由 binding
提供,倾向于使用 TinyGo
而非 Google 官方的 Go
。(虽然 Google 也开始逐渐提供 WASI
的支持,但还处于一个非常早期的阶段)
关于 Rust
,是探针组熟悉的语言,编译产物文件小,高性能,无 GC,适合作为相对变化不频繁的部分的实现,例如公共库和探针功能辅助插件。由于学习/编写/调试/阅读相对效率较低,插件的主要部分不适合使用 Rust
编写。考虑到 Rust
写些简单的逻辑也很爽,某几个辅助功能插件用 Rust
来写也无不可。
关于 Zig
,考虑到尚未到达 1.0 版本,不适合生产环境使用。
由于 WASM 的模型限制,异步特性略微受到限制,可能产生额外开销。例如在 TinyGo 文档中关于并行有以下描述:
Support for other platforms (such as WebAssembly) is a bit more limited: calling a blocking function may for example allocate heap memory.
虽然这些语言都可以称得上对 WebAssembly
支持的好,但实际的选择还需要考虑许多其他因素,例如 runtime
的支持,WASI
的支持程度,binding
的编写便捷性,以及我们的第二个问题。
关于第二个问题,现有的方案有四种可选,即手动使用 runtime 提供的 API 维护、使用waPC进行跨模块的过程调用、使用Extism封装好的插件调用逻辑、使用wit-bindgen提供的对 WIT
格式的支持。其中第一种手动维护的支持是最稳定的,第二种和第三种是基于第一种提供的不同表现形式的封装,第四种 WIT 是 WASI 的 preview2 的一部分,即将成为正式标准。
WIT即 Wasm Interface Type
,是字节码联盟继第一阶段实验后确定的第二阶段接口标准格式,为 WebAssembly
组件模型的导入导出提供了统一的高阶数据类型表示标准。如果不出意外的话,第二阶段确定的内容在第三阶段也不会更改(按照官方的说法,第三阶段只是为了提供一个最后解决问题的机会),而是修复一些问题以后直接成为最终的正式标准。
有一部分厂商已经开始在自己的商业化产品中使用 WIT
支持,例如著名的无服务器厂商 Fermyon 的产品Spin。
除了常见的整型、浮点、字符串之外,WIT
还提供了包括 tuple
、option
、result
、interface
、record
、variant
、enum
、union
、flags
等高级类型,着实吸引人。与 wit-bindgen
和 Rust
一起食用十分舒适。
虽然前面三种有更长的发展时间(因此可能更稳定),但我们打算跟随未来标准,参与到标准的生态共建中来。
在对比了多种编程语言在性能、安全性、易用性等方面的优缺点之后,我们最终选择了 TinyGo
作为主推的插件开发语言,同时辅以 Rust
。
TinyGo
在编译产物文件大小、性能表现和易用性方面具有明显优势,适合用于插件的主要部分。而 Rust
则在高性能和安全性方面表现出色,适合作为相对变化不频繁的部分的实现,例如公共库和探针功能辅助插件。
除此之外,我们对 zig
和 nim
也很感兴趣,会持续关注其是否能够达到生产要求。
通过为牧云插件系统选择合适的开发语言,我们可以更好地满足主机安全系统的需求,提高插件的性能和安全性,同时降低开发和维护成本。在未来,我们将持续关注 WASM
生态的发展,优化和改进插件系统,以满足不断变化的安全需求。
系列文章目录:【预告】主机Agent插件引擎开发故事汇总