15cilium之Envoy代理注入和go扩展

Cilium 是一个基于 eBPF 的高性能网络策略引擎,它非常擅长处理底层网络,比如 L3 L4 的规则。但当涉及到更复杂的 L7 级别策略,比如 HTTP 路由、内容过滤、安全策略时,就需要一个强大的 L7 代理。

这就是 Envoy 的用武之地。Cilium 通过集成 Envoy,构建了一个强大的网络代理,专门用来执行集群里定义的 L7 网络策略。这个集成代理是内置于 Cilium 镜像中的,开箱即用。那么,Envoy 在 Cilium 中是怎么部署的呢?

默认情况下,当您启用 Cilium 的 L7 功能,比如 Ingress 或者 L7 网络策略时,Cilium Agent 会自动在自己的 Pod 里启动一个 Envoy 进程。这个 Envoy 就负责处理该节点上的所有匹配 L7 策略的流量。这种方式的好处是简单,但缺点也很明显:如果 Cilium Agent Pod 重启了,比如升级或者故障,那么依赖这个 Agent 的 L7 流量就会中断。这显然不是我们希望看到的。

所以,Cilium 提供了另一种选择:将 Envoy 从 Agent Pod 中剥离出来,作为一个独立的 DaemonSet,叫做 cilium-envoy。为什么要把 Envoy 单独部署成 DaemonSet?好处多多。

  • 性能隔离。Agent 和 Envoy 可以独立设置 CPU 和内存限制,避免互相干扰,让它们都能跑得更顺畅。
  • 日志不再混杂。Envoy 的应用日志和 Cilium Agent 的日志分开,排查问题时定位更清晰。
  • Envoy 有了专属的健康检查,能更及时地发现自身问题。
  • 部署方式也更显式。安装时就明确部署了 Envoy,而不是像嵌入式那样按需启动,管理起来更可控。

这对于需要高可用性的 L7 服务来说,简直是福音。现在,我们来聊聊一个更酷的功能——Go 扩展。这可是 Cilium 的一个 Beta 阶段特性,潜力无限。

它的核心思想是:开发者只需要编写少量的 Go 代码,专注于解析你自己的 L7 协议,比如某个内部的 RPC 协议,或者某个特定的数据库协议。而 Cilium 的框架、高性能、强大的 L7 策略语言、访问日志,甚至未来的 kTLS 加密流量可见性,都由 Cilium 自己搞定。

proxylib_logical_flow.png

你看这张图,绿色的框就是你的 Go 代码,负责解析协议;而 Cilium 和 Envoy 的框架负责处理数据流、应用策略、记录日志。开发者只需要专注于协议本身,其他底层的脏活累活交给 Cilium 和 Envoy。

这个 Go 扩展怎么一步步开发呢?大致可以分为几个步骤。

  • 首先,你要明确你的协议安全策略是什么,比如哪些操作允许访问哪些资源。
  • 然后,深入理解协议的细节,比如数据怎么编码、帧怎么分隔、有哪些字段。
  • 接着,看看有没有现成的开源代码可以用。
  • 然后,按照 Cilium 的开发指南,搭建好环境,创建一个新代理的骨架。核心就是实现 OnData 方法,这是解析数据流的关键。
  • 之后,用单元测试来驱动开发,确保解析正确。逐步添加更复杂的解析逻辑,比如提取关键字段。
  • 然后,实现策略的加载和匹配,让代理能根据规则做决定。拒绝请求时,不能只丢掉,还要注入错误响应告诉客户端。别忘了记录访问日志,方便审计。
  • 最后,通过手动测试和运行时测试来验证,确保一切正常。

我们先来看前两个步骤。

  1. 第一步,确定基本策略模型。你需要思考,对于你的协议,安全策略应该怎么定义?通常,协议都有操作和资源的概念。比如,HTTP 请求有 GET、POST 等方法,这是操作,URL 是资源。数据库协议有 SELECT、INSERT 等动作,作用于数据库表名。队列协议有生产、消费等操作,作用于队列。常见的策略就是白名单,允许特定的操作访问特定的资源。有时候,资源路径里可能包含变量,比如用户ID,这时就需要用正则表达式来匹配。
  2. 第二步,理解协议细节。你需要去啃协议的官方文档,了解它的编码方式、帧结构、请求和响应字段的含义。还要看消息流,比如请求和响应的顺序,是否支持 pipelining。以及,当策略拒绝请求时,应该返回什么类型的错误信息。
  3. 第三步,搜索现有解析器。看看有没有开源的 Go 库或者代码,可以直接拿来用或者参考一下。比如 Redis 有 tidwall/recon,MySQL 有 Vitess。Wireshark 的解析器虽然写的是 C,但也能提供很多启发。不过要注意,客户端的解析代码和代理需要的解析代码可能不一样,代理通常需要解析更多类型的请求。
  4. 第四步,遵循 Cilium 开发指南。你需要先克隆 cilium proxy 仓库,然后 vagrant up 启动一个预配置好的开发环境,再 vagrant ssh 进去。所有的开发工作都在 proxylib 目录下进行。
  5. 第五步,创建新代理骨架。很简单,复制一个现有的目录,比如 r2d2,然后改名。把里面的文件也改成你的协议名,比如 newproto.go,newproto_test.go。别忘了修改 proxylib.go,导入你的新包。
  6. 第六步,实现核心函数 OnData。这个函数是整个解析器的心脏,它会被不断调用,处理来自客户端或服务器的数据。它有两个参数:data 是一个字节切片数组,代表收到的数据;reply 是一个布尔值,表示当前是请求方向还是响应方向。你需要根据你的协议逻辑,返回 PASS、MORE、DROP 或者 ERROR。PASS 是允许数据通过;MORE 表示还需要更多数据才能完整解析;DROP 是拒绝请求;ERROR 是发现协议错误,需要终止连接。刚开始可以先简单处理,比如打印收到的数据,然后 PASS 通过。
  7. 第七步,单元测试驱动开发。强烈建议用单元测试来驱动开发。Cilium 的框架里已经内置了很好的测试工具。你可以用 CheckOnDataOK 方法,给它一个模拟的请求数据,然后告诉它你期望 OnData 返回什么结果。这样可以快速验证你的解析逻辑。数据来源可以是 Wireshark 抓包,或者直接写 ASCII 字符串或 Hex 字符串。运行测试用 go test。
  8. 第八步,添加高级解析。现在,你需要根据协议的细节,解析出关键字段,比如操作和资源。还要考虑各种复杂情况:比如数据包不完整怎么办?多个请求挤在一个包里怎么办?畸形的请求怎么处理?有些协议可能需要维护状态,比如 Cassandra 的 prepared queries,就需要在解析器里存状态。
  9. 第九步,添加策略加载和匹配。现在你已经能解析协议了,接下来就要让它能根据策略做决定。你需要定义一个策略规则对象,比如 R2d2Rule。然后实现 Matches 函数,这个函数就是判断一个请求是否符合某条规则。还要实现 NewProtoRuleParser,用来把配置文件里的策略转换成你的规则对象。在 OnData 里,你需要调用 p.connection.Matches() 来判断请求是否匹配当前策略。如果不匹配,就返回 DROP。别忘了更新你的单元测试,加入策略测试,确保策略生效。
  10. 第十步,注入错误响应。简单地 DROP 请求是不够的,客户端会一直等着,不知道为什么失败。你需要在拒绝请求时,通过 p.connection.Inject 方法,向客户端发送一个符合协议的错误响应,比如 403 Forbidden,告诉它访问被拒绝了。
  11. 第十一步,添加访问日志。Cilium 提供了访问日志功能,记录每个请求的处理结果。你需要在 OnData 或者其他合适的地方调用 p.connection.Log 方法来记录日志。日志里可以包含协议类型、请求的字段、操作、资源等信息,方便后续的审计和分析。
  12. 第十二步,手动测试。理论和代码都跑通了,现在需要在真实环境中跑一跑。启动你的服务器和客户端容器,让它们都连接到 cilium-net 网络。用 cilium-dbg endpoint list 查到服务器的 IP。然后用 docker exec 进入客户端容器,用你的客户端工具去连接服务器。尝试执行一些允许的请求和一些被拒绝的请求,看看代理的行为是否符合预期。
  13. 第十三步,运行时测试。手动测试还不够,Cilium 提供了运行时测试框架,可以更全面地模拟真实环境。你可以复制一个现有的运行时测试,比如 Cassandra 的,然后修改它来测试你的新协议。在运行时测试里,你可以配置策略,比如允许某些查询,然后运行测试,看策略是否真的生效。
  14. 第十四步,审查协议规范。最后,别忘了回过头去仔细阅读协议的完整规范,看看有没有遗漏的边缘情况。比如,协议有没有特殊的功能,或者对空值、异常情况的处理。把这些边缘情况也用单元测试覆盖起来,确保你的解析器足够健壮,不会因为一些奇怪的输入而崩溃或者漏掉安全检查。
  15. 第十五步,编写文档。如果你的扩展对其他开发者有用,最好写一份文档。至少,运行时测试里的例子可以作为基本的文档。当然,写一份更详细的用户指南,介绍怎么用、有哪些策略选项,那就更好了。记得更新 Cilium 的文档索引,让大家能找到你的新功能。

好了,关于 Cilium 与 Envoy 的集成以及 Go 扩展的开发,我们就先聊到这里。希望这些内容能帮助大家更好地理解和应用这些技术。