本文将介绍我们在牧云插件系统中技术选型中的一些挣扎和考虑。我们将探究到底设计一个主机安全系统的插件系统应该如何考虑,如何做出选择,以及为什么最终选择了WASM作为牧云插件系统的底层技术。
这是系列文章“主机Agent插件引擎开发故事”的第三篇,后续将会持续更新。该系列文章将带领您深入探究长亭牧云团队主机Agent插件引擎的开发历程,内容涵盖技术选型、插件接口设计、组件通信框架等多个方面,并详细讲解背后的原理和实现方式,无论您是网络安全专业人员还是对技术开发感兴趣的读者,都可以从中得到收获。我们希望通过分享在开发过程中面临的挑战、解决方案以及实践经验,提供深入见解和有价值的技术参考,帮助读者了解如何构建高效可靠的安全产品,共同推动安全技术社区的发展。
根据之前两篇文章(【揭秘牧云插件开发者的创新之路:从无法解决的问题到“妙趣横生”】和【牧云插件系统面向未来的设计原则】)的介绍,牧云作为一个主机安全系统,需要高效、可靠、稳定的 Agent
核心程序,同时还要能够随时更新安全能力、灵活适应不同的运行环境、检测日新月异层出不穷的网络攻击手法。因此需要一种基础安全能力和安全业务相分离的技术架构。这种架构对应到软件工程上,就是我们熟悉的插件系统。
每个插件是一个独立的函数,是宿主程序的一部分。
其实很难说这是一种插件形态,看起来会更像是 Agent
程序代码的一种组织方式。但不得不承认的是,这确实是理论上性能最好的方案,但同时也是对稳定性最大的打击。从灵活性的角度来看,只要人工保证每个函数的代码的组织是独立的,似乎就能通过更新 Agent
来更新安全能力。尽管更新 Agent
导致安全能力中断本身看起来似乎就有些不可接受。
优点: 性能最好,几乎没有插件与宿主的通信成本。
缺点: 稳定性相对难以保证,更新容易导致安全能力中断。
每个插件是一个独立的协程/线程,是宿主程序的一部分。
同样看起来也不太像是插件,相比于形态一提高了一点点隔离性,获得了一点点对插件的控制能力,但不多。
优点: 性能很好,方便异步运行,通信增加了一点点同步成本。
缺点: 对插件的控制力不足,仍然不太容易保证稳定性,更新同样会导致安全能力中断。
每个插件是宿主程序的一个子进程,实现与宿主程序相互独立。
一个子进程,非常的独立,在操作系统中几乎不能更独立了。子进程崩溃对 Agent
稳定性可以说完全没有影响。跨进程通信成本高了一点。
优点: 最独立的插件形式,对 Agent
稳定性维护非常有利。
缺点: 通信成本略高,对子进程的控制力不足,意外 Bug 容易导致更严重的事故。
宿主程序与一个独立的脚本执行引擎交互。每个插件是一个脚本程序。
在独立的子进程中用脚本执行引擎来执行插件脚本(而非子进程本身就是插件),以便于通过脚本执行引擎来增强对插件行为的控制能力。
优点: 在形态四的基础上加强了对插件的控制,大大降低了严重意外事故发生的概率。
缺点: 是对性能的重大打击,在跨进程通信的基础上又增加了解释执行的损耗,可能无法响应忙碌系统中的大量安全事件。
宿主程序内部嵌入了一个脚本执行引擎。每个插件是一个脚本程序。
获得脚本执行引擎的控制能力的同时,去除跨进程通信的消耗。
优点: 相对形态四通信成本有所降低。
缺点: 解释执行真的有点慢。
宿主程序内部嵌入了一个虚拟机。每个插件是一段在虚拟机上执行的字节码。
通过将脚本执行引擎替换成基于虚拟机的插件执行引擎来提高插件执行效率。可选的加速方法包括 JIT
(Just In Time,即时编译)和 AOT
(Ahead of Time,运行前编译)技术。如果选一个性能好的虚拟机,最好再支持点加速技术,似乎就很不错了。其实这就是牧云插件系统当前选择的内嵌 LuaJIT VM
来实现插件系统的原因。
优点: 相对形态五提高了插件执行性能理论上限,有利于在安全事件增加时保持安全响应能力。
缺点: 看起来没有缺点,具体参照系列的第二篇文章。
虽然看似形态六已经全面胜出了,但面对现实中遇到的实际问题,牧云团队内部针对以上各种形态的选择仍然存在许多不同的观点:
Agent
里。Agent
核心代码必须保持稳定,所以不能把插件写到 Agent
里。Agent
稳定性不受插件影响。Agent
程序的核心设计目标之一。关于这些观点,以下是一段模拟的对话,参与者均为资深网络安全专业人员,角色分别为领导 A、看重性能和稳定性的 Agent
开发人员 B、看重实现隔离性和灵活性的插件开发人员 C:
A: 大家都知道我们面临的问题:需要找到一个既能保证性能和稳定性,又能实现隔离性和灵活性的插件系统方案。我们来一起讨论一下吧。
B: 是的,性能和稳定性非常重要。我倾向于形态一,每个插件都是独立的函数,通信成本较低。我们可以通过严格的代码审查来保证稳定性。
C: 我觉得形态四也是个不错的选择,每个插件都在独立的进程中执行,安全隔离。这对于主机安全系统来说是个重要因素。
B: 但进程隔离会增加通信成本,我们可以考虑形态二,协程或线程。通过合理的同步策略,我们可以实现良好的隔离性和稳定性。
C: 除了这些方案,我们还可以考虑基于脚本执行引擎的形态五。这样可以让插件更加灵活,便于调试和维护。
B: 脚本引擎的性能可能会受到影响。我觉得形态六是个折中的方案。虚拟机可以提供较好的性能与控制,同时确保主机安全。
C: 说得对。虚拟机可以提供较好的隔离性,有利于主机安全。我们可以选择一个支持JIT或AOT技术的虚拟机,如LuaJIT,以保证性能。
B: 好的,我同意尝试形态六。我们可以在实际应用中观察性能表现,如有问题,我们再进行调整。
A: 我们先试用形态六,采用虚拟机作为插件系统的基础。如果实际效果不理想,再根据实际情况调整。
讨论到最后,大家多少都有点绷不住,到底怎么办的问题萦绕在每个人心中。其实以上形态在业界都有广泛采用的例子,适合其各自的场景,都不能说是坏的选择,但到底哪一种才是对一个主机安全系统最合适的选择的确需要认真考虑。
观察友商的实际选择,其实一些我们可能会认为显然不是最优的方案却真实存在着,各家的权衡取舍实际原因到底是什么真的很难猜测。比如有的用 Java
实现 Agent
,有的用 C++
实现 Agent
;有的用 Python
实现插件,有的用 Bash
实现插件,有的用 Perl
实现插件。
其实牧云当前的选择,即 Go
语言编写 Agent
,Lua
实现插件,本身就是一种少见的选择。理论上,Go
是一种高效灵活跨平台的语言,Lua
以稳定和高性能著称,当初牧云立项的时候这个选择看起来无可挑剔。
追根到底,其实我们真正需要考虑的是我们之前的插件系统解决了哪些问题,而新的选择有多少成本又能带来多少价值(持续思考中……)。
最终决定的插件架构是以上各种考虑的折衷,目前看来可以获得多种插件系统形态的好处。也就是形态七。除了形态本身的变化,通过改用 Rust
实现 Agent
程序可以获得解决兼容性问题的能力,并避免 CGO
带来的数据拷贝,通过 WASM
获得上一篇文章描述的诸多好处。
宿主程序与一个独立的虚拟机引擎通信,插件运行在虚拟机上。
简单来说,我们不满足于插件跑得飞起,还想要将其限制在指定的资源下。CPU
占用不能超过指定的阈值,内存不能超过指定的阈值,IO
不能超过指定的阈值,整体资源占用消耗平稳,优先保障业务,但同时安全能力不能差。所以设计实现了一整套插件的限制和调度机制,但正如进程对自己的协程控制能力很差,我们对虚拟机执行所在的那个线程的控制能力有限,想要在满足以上条件的同时进一步提高安全效果是比较难的。而将运行插件的虚拟机搬到独立的进程上似乎是一个可行的选择。为了缓解进程间通信通信成本,经过分析,对插件需要访问的资源进行分拆,大部分在虚拟机进程上直接完成,少数需要 Agent
主程序协作的数据则通过全新设计的进程间通信 RPC
框架完成。至于具体如何解决的详细情况,那就是一个新的故事了。
优点: 解决了形态六的缺点的同时,缓解了形态四的缺点。
缺点: 迁移全套安全能力需要一点时间,而且可能存在我们没有看到的风险。
我们在牧云主机安全系统的开发过程中,针对插件需求进行了深入探讨,并对多种插件系统形态进行了分析和比较。通过团队内部的选型争论和业界方案观察,我们意识到在选择插件系统时,需要综合考虑解决问题、成本与价值等多方面因素。最终,我们根据项目需求和团队的技术特点,作出了独特的选择。
在整个过程中,我们始终以满足系统的安全、性能、扩展性和稳定性为出发点,希望通过构建一个强大而灵活的插件系统,奠定持续优化和改进牧云主机安全系统的基础,为用户提供更加安全、稳定的服务。
系列文章目录:【预告】主机Agent插件引擎开发故事汇总