Istio Ztunnel 架构解析:Ambient 网格中节点代理的设计决策、HBONE 协议与流量路由
本文基于 Istio 仓库中的架构文档 architecture/ambient/ztunnel.md,系统梳理 Ambient 模式核心组件 Ztunnel 的设计动机、xDS 定制配置协议(Address 与 Authorization 资源)、基于 HTTP CONNECT 的 HBONE 隧道协议、三条流量路径(15001/15006/15008)的路由逻辑与 SPIFFE 证书管理机制,并结合仓库中 HBONE 协议库 与 Workload API 定义 等源码,帮助读者建立对"节点级轻量代理"这一 Istio 演进方向的完整技术认知。
背景与动机:从 Sidecar 走向"远程代理"
文档开宗明义:Ztunnel 是 Ambient 模式下的节点代理(node-proxy)组件,其实现动机主要来自两个方面。
第一个、也是最根本的动机,是实现 Ambient 网格的终极目标——Waypoint 代理。出于本文档范围之外的种种考量,Istio 希望从 Sidecar 架构迁移到"远程代理"(remote proxy)架构。但这里存在一个明显的难题:如何在保持 Istio 所依赖的零信任(zero-trust)属性的前提下,把流量正确送达这些远程代理?Ztunnel 正是回答这个问题的手段。
第二个动机是提供一个更平滑的"从零到有"(Zero → Getting some value)的接入路径。历史上,Istio 的使用基本是"全有或全无"(all-or-nothing)的。而 Ambient 模式回应了一个普遍诉求:"我只想先在整个集群启用 mTLS,之后再考虑采用服务网格的其他能力。"
在 Kubernetes 上运行时,Ztunnel 与 CNI 的生命周期协同另有专门文档,可参见 Ztunnel CNI 生命周期文档;关于 PeerAuthentication 的实现细节则见 Peer Authentication 实现文档。
设计目标:轻量、安全、绝不破坏用户
文档将 Ztunnel 必须满足的目标归纳为三条:
- 不破坏用户(Not break users)。部署 Ztunnel 必须保留全部现有 Kubernetes 行为,包括 UDP、不规范的 HTTP、服务端先发协议(server-first protocols)、StatefulSet、外部服务等。显式选择(opt-in)的行为变更是可以接受的,例如引入 Istio 的多集群语义。
- 确保网格内工作负载之间的流量以 Istio 身份进行安全加密。
- 足够轻量,不成为采用的限制因素。这给 CPU、内存、延迟和吞吐预算带来了比传统 Istio Sidecar 严格得多的约束。
文档同时强调了一个关键的设计立场:Ztunnel 刻意不是功能丰富的数据面。恰恰相反,"激进地小的功能集"才是让 Ztunnel 可行的核心特性。它有意不提供 L7(HTTP)能力,因为这类能力大概率会违反上述目标又毫无贡献。服务网格传统上关联的丰富功能被推迟到 Waypoint 代理去实现——Ztunnel 的核心职责就是把流量安全地送达 Waypoint。
代理实现选型:Rust 胜出
在最初的实现阶段,Ztunnel 实际上以三种方式分别实现过:一个自研的 Rust 实现、一个自研的 Go 实现,以及基于 Envoy 的实现。
最终经过评估,团队决定采用 Rust 实现。文档给出的理由很直接:Rust 实现带来的性能收益大到"不能留在桌上",同时提供了针对特定需求做深度调优的空间。这也呼应了前面"轻量"的目标:传统数据面代理的资源开销无法满足节点级部署的预算。
从仓库结构看,Istio 主仓库本身是 Go 语言项目,Rust 版的 Ztunnel 数据面实现独立于本仓库维护;本仓库中与 Ztunnel 直接相关的是其配置协议定义、部署 Chart(见 ztunnel Chart)以及 Go 侧的 HBONE 工具库。
配置协议:xDS 传输协议 + 定制资源类型
Ztunnel 需要被动态配置以决定如何处理流量。为此,Istio 选择了 xDS 传输协议——基于团队的既有专长和基础设施,且该协议非常契合需求。
但关键的取舍在于:只复用 xDS 的传输协议,不复用 xDS 的资源类型(如 Clusters、Listeners)。文档解释了原因:在 Istio 的经验与测试中,这类通用资源类型迫使他们以低效的方式表达数据。Ztunnel 并非通用组件,它目标极窄,因此可以利用这一点设计更高效的协议——这对达成资源占用目标是决定性的。
文档举了一个直观的例子:在 Envoy 中配置 Istio mTLS 大约需要 50 行 JSON(当然底层是 Protobuf,但量级依然相关)。由于 Ztunnel 可以把 Istio 语义"内置",就无需在网络上编码全部这些信息。例如,一个 Istio 专有字段 ExpectedTLSIdentity: spiffe://foo.bar 就能以极小的成本编码同样的语义。
在团队测试中,即便采用最宽泛的统计口径,定制类型相比 Envoy 类型在大小、内存分配次数和 CPU 耗时上都拥有约 10 倍的优势。此外,定制类型更清晰、类型更严格——若使用 Envoy 类型,大量信息只能塞进无类型的 metadata 映射中。
基于此,Ztunnel 支持两种 xDS 资源:Address 和 Authorization。
Address 资源类型
Ztunnel 消费的主要配置是 Address 资源(定义于仓库 pkg/workloadapi/workload.proto)。顾名思义,一个 Address 表示一个特定的 IP 地址,它可以是一个 Service,也可以是一个 Workload。
Address 类型有两条设计目标:
- 支持(但不强制)按需查询(on-demand lookups)。具体来说,Ztunnel 应能向控制平面发起询问:"我收到了一个发往 1.1.1.1 的请求,1.1.1.1 是什么?"。小规模集群用不上这个能力,但对于端点数高达百万级的超大集群长尾场景,把全量端点复制到每个 Ztunnel 是不现实的,按需查询因此至关重要。
- 不特定于客户端(not client-specific)。Istio Sidecar 历史上存在大量与客户端相关的 xDS,例如把 xDS 客户端的 IP 回写进 xDS 响应,这使得控制平面的高效实现(尤其是缓存)极其困难。Address API 在实践中基本做到引用完全限定:IP 地址(通常)关联网络标识、节点名关联集群标识等。
文档进一步说明两类子资源:
Workload:表示一个工作负载(通常是Pod或WorkloadEntry)的一切信息,包括其 IP 地址、身份(identity)、元数据(名称、命名空间、app、version 等),以及是否关联了 Waypoint 代理。Service:表示一个服务(通常是Service或ServiceEntry)的一切信息,包括其 IP 地址、端口,以及关联的 Waypoint 代理(如果有的话)。
Authorization 资源类型
Ztunnel 消费的次要配置是 Authorization 资源(定义于仓库 pkg/workloadapi/security/authorization.proto)。该资源旨在表达 Ztunnel 所支持的、相对较小的一类授权策略集合,注意:只支持 L4 资源。
查看该 proto 文件可以印证文档描述:Authorization 消息包含 name、namespace、scope(取值 GLOBAL / NAMESPACE / WORKLOAD_SELECTOR)、action(默认 ALLOW,可选 DENY)、一组 groups(组间为 OR 关系)、dry_run 标志以及扩展字段;Group 内的规则为 AND 关系;Match 支持 namespaces、service_accounts、principals、source_ips、destination_ips、destination_ports 等匹配条件,且每个条件均提供 not_* 反义形式(详见 authorization.proto 中 Match 与 Scope、Action 的定义)。
文档特别指出 API 中一个有趣的设计点——策略与工作负载的关联方式。Istio 的 AuthorizationPolicy 使用 label 选择器,但 Workload API 有意不把这些选择器发送出去,以控制配置体积。把"被选中的工作负载列表"放进策略本身是最直观的方案,但策略会因此频繁更新(工作负载变化远比策略变化频繁)。最终选择了相反的做法:由每个工作负载列出选中它的策略。在"策略变化频率远低于工作负载"的常见情况下,这种方向更高效。且该机制仅适用于选择器类策略;命名空间级与全局策略无需在 Workload API 中逐一列出即可处理。
流量重定向(Redirection):安全关键路径
Ztunnel 的目标是透明地加密并路由用户流量,因此必须捕获进出"网格内" Pod 的所有流量。文档强调这是一项安全关键任务:如果 Ztunnel 可以被绕过,那么授权策略就可以被绕过。
重定向机制必须满足三条要求:
| 方向 | 端口 | 行为 |
|---|---|---|
| 出向(egress) | 15001 | 网格内 Pod 的全部出向流量重定向到节点本地 Ztunnel 的 15001 端口。若流量目标是一个 Service,必须保留 Service IP |
| 入向(ingress) | 15008 | 进入网格内 Pod 15008 端口的流量被视为 HBONE 流量,重定向到节点本地 Ztunnel 的 15008 端口(后文详述) |
| 其他入向 | 15006 | 进入网格内 Pod 的其他所有流量,无论原始目标端口是什么,均重定向到节点本地 Ztunnel 的 15006 端口 |
需要说明:原文档在重定向一节末尾保留了 TODO("fill in implementation details of how redirection is actually implemented"),即具体实现细节(如 iptables/nftables 规则)尚未在该架构文档中展开。从源码结构看,本仓库中 CNI 侧的 nftables 规则生成代码 与 istio-iptables 工具 承载了相关的流量劫持实现,可作延伸阅读。
HBONE:HTTP-Based Overlay Network
协议本质
除直通流量外,Ztunnel 支持 "HBONE"(HTTP-Based Overlay Network,基于 HTTP 的叠加网络)协议。文档澄清:这与其说是一个新协议,不如说是 Istio 为"网格内客户端与服务器通信的预期方式"起的名字。
HBONE 就是在标准 mTLS(使用网格 SPIFFE 证书)之上、在知名端口 15008 上运行的标准 HTTP CONNECT 隧道。目标地址放在 :authority 头中,还可以附带额外的头。目前仅支持 HTTP/2,HTTP/1.1 与 HTTP/3 在计划中。
为什么不用 SNI
当前 Istio 客户端不设置 SNI,服务器端也忽略 SNI,这让 Ztunnel 难以决定该用哪张证书。处理方式很巧妙:请求发往 DestinationPod:15008 并被重定向到 Ztunnel,而不是发往 ZtunnelPod:15008——然后通过原始目的地(original destination)提取来确定应使用哪张证书。不用 SNI 的原因有二:SNI 中非法使用 IP 地址,且不存在其他既有标准格式来表达所需信息;此外,借助重定向机制还能降低客户端需要知道目的地 Ztunnel 地址的要求。
下图(原文档自带)展示了一个出向请求示例,"target" 路径是客户端发送的目标,"actual" 路径是重定向后的真实网络流向:
graph LR
subgraph Client Node
Client
CZ["Ztunnel"]
end
subgraph Server Node
Server
SZ["Ztunnel"]
end
Client--Plain-->CZ
CZ-."HBONE (target)".->Server
CZ--"HBONE (actual)"-->SZ
SZ--Plain-->Server
连接池化(Pooling)
用户连接可以复用在共享的 HBONE 连接上,通过标准 HTTP/2 池化实现。池化的键为 {源身份, 目的身份, 目的 IP}。
头字段(Headers)
Ztunnel 在 HBONE 中使用以下知名头:
| Header | 用途 |
|---|---|
:authority |
CONNECT 中必需,表示目标目的地 |
Forwarded |
对出向请求,携带原始源 IP。注意由于 Ztunnel 在多数情况下会伪装 IP,这通常与实际看到的 IP 相同;对入向请求,仅用于来自 Waypoint 的流量(Waypoint 可信且无法伪装 IP) |
Baggage |
(实验性,可能被移除)包含源/目的工作负载的元数据,用于遥测 |
Traceparent |
(实验性)维护连接级跟踪信息。注意这是连接的跟踪,与用户自身 HTTP 请求的跟踪不关联,但有助于跨 Ztunnel 追踪同一条连接 |
仓库中的 HBONE 实现
本仓库的 pkg/hbone 目录提供了 HBONE 的 Go 参考实现(标注为不稳定 API),与文档描述一一对应:
- 客户端拨号器:dialer.go 中
NewDialer构造一个将连接经 HBONE 代理转发的Dialer,其hbone函数内部正是发起CONNECT请求并设置r.Host = address(对应:authority承载目标地址的设计),随后双向拷贝数据。用法示例:
d := hbone.NewDialer(hbone.Config{
ProxyAddress: "1.2.3.4:15008",
Headers: map[string][]string{
"some-additional-metadata": {"test-value"},
},
TLS: nil, // 实际生产环境强烈建议使用 TLS
})
client, _ := d.Dial("tcp", testAddr)
client.Write([]byte("hello world"))
- 服务端:server.go 中
NewServer()返回一个 HTTP/2 服务器,其核心逻辑在handleConnect中——收到CONNECT请求后立即 Flush 响应头以尽早接收请求体,再向r.Host建立到目标地址的 TCP 连接,成功后返回 200 并双向拷贝。该实现直观印证了"Inbound 路径中 TLS→CONNECT→用户连接"的分层结构。仓库还附带命令行工具:go install ./pkg/test/echo/cmd/client(HBONE 客户端)与go install ./pkg/test/echo/cmd/server(HBONE 服务端,默认 15008 端口),可用于手工验证 HBONE 隧道行为。
流量路由:三条主路径
基于前文三条重定向路径,Ztunnel 处理三类主要流量。
Outbound(出向,端口 15001)
离开 Pod 的请求走 Ztunnel 的"outbound"代码路径。这是 Ztunnel 大部分逻辑所在。
由于 Ztunnel 工作在 L4,它只有目标 IP/端口(通过 SO_ORIGINAL_DST 恢复)。该目标可能是 Service 的 IP、某个 Pod 的 IP,或集群外部地址。Ztunnel 会从其配置的 Address 集合中查询目的地。
- 未知地址或不属于网格的工作负载:流量原样直通。为更透明,Ztunnel 会伪装原始源 IP;并尽可能使用
splice让代理更高效。 - 网格内流量则更复杂,分三种情况:
- 若目的地关联了 Waypoint 代理,必须经由 HBONE 发给 Waypoint。此时要保留原始目的地 Service IP,因为 Waypoint 比 Ztunnel 更擅长挑选后端 Pod。注意:应用本身可能已内置 Kubernetes 原生路由并把 Service IP 解析为具体 Pod(Sidecar 代理即如此),此时没有 Service 信息可用,Ztunnel 将直接使用收到的目的地 IP(即 Pod IP)。
- 若目的地在本节点上,请求走"快速路径"(fast path),直接转化为一次入向请求。语义等价于把请求发回给自己,但更高效,也降低了 Ztunnel 内部复杂度。
- 否则,Ztunnel 通过 HBONE 把请求转发到目的地;若目的地是 Service,则先解析为具体 Pod IP。
所有情况下,Ztunnel 都会伪装原始源 IP。
Inbound Passthrough(入向直通,端口 15006)
进入 Pod 但非 HBONE 传输(即目标端口 != 15008)的流量,由"入向直通"代码路径在 Ztunnel 的 15006 端口处理。逻辑相对直接:
- 首先检查该流量是否被允许——它可能因 RBAC 策略被拒绝,尤其是来自
STRICT模式的强制(该模式拒绝明文流量)。 - 若允许,则转发到目标目的地。
Hairpin(发夹)问题:若目的地配置了 Waypoint,那么能到达"入向直通"路径意味着该 Waypoint 被绕过了。文档注明其处理方式尚在讨论中。
Inbound(入向 HBONE,端口 15008)
经 HBONE 进入 Pod 的流量由"入向"代码路径处理。入向请求具有多层结构:TLS 包裹 HTTP CONNECT,CONNECT 再包裹用户连接。解包流程:
- 终止第一层 TLS。过程中需要代表目的地工作负载选择正确的证书——如前文 HBONE 一节所述,依据是目的地 IP。同时强制对端持有有效的网格身份(但暂不断言是哪一个身份)。
- 终止 CONNECT。从 头字段可知目标目的地。若目标目的地配置了 Waypoint,则强制要求请求必须来自该 Waypoint,否则拒绝;若无 Waypoint,Ztunnel 对该请求执行 RBAC 策略检查。
- 所有检查通过后,Ztunnel 与目标建立连接,并伪装源 IP(对 Waypoint 流量取自
Forwarded头,否则取入向 IP)。连接建立后返回 HTTP 200,并在隧道与目的地之间双向拷贝数据。
证书管理:代表工作负载持有证书
Ztunnel 的证书基于标准 Istio SPIFFE 格式:spiffe://<trust domain>/ns/<ns>/sa/<sa>。
但关键区别在于:证书的归属身份是真实用户工作负载的身份,而非 Ztunnel 自身的身份。这意味着 Ztunnel 同一时刻持有多张不同证书——节点上每运行一个唯一身份(ServiceAccount)就对应一张。
取证书时的流程与约束:
- Ztunnel 以自身身份向 CA 认证,但请求的是其他工作负载的身份。CA 必须强制校验 Ztunnel 是否有权请求该身份——不属于本节点运行的身份请求会被拒绝。这一点至关重要,确保单个节点被攻破不会连累整个网格。
- 该 CA 强制由 Istio CA 实现,也是任何与 Ztunnel 集成的第三方 CA 的硬性要求。
- 实现细节:Ztunnel 使用 Kubernetes ServiceAccount JWT Token 向 CA 认证,token 中编码了 Pod 信息,正是这一点使上述"代表节点上工作负载取证书"成为可能。
证书获取与轮转策略:
- Ztunnel 会为节点上的所有身份请求证书,依据是其收到的 Workload xDS 配置。
- 发现节点上新身份时,会将其以低优先级入队获取(一项优化);但若某个请求恰好需要尚未获取的身份,则会立即请求。
- Ztunnel 还负责这些证书(通常 24 小时有效期)的临近过期轮转。
遥测(Telemetry)
Ztunnel 针对其支持的 4 项 TCP 指标,输出完整的 Istio 标准指标集。由于 Ztunnel 刻意不承载 L7 能力,其指标范围聚焦于 TCP 层连接,与"轻量数据面"的定位一致。
小结与延伸阅读
Ztunnel 的设计可以浓缩为一句话:用最小的功能集、最高的效率,把流量安全地送到 Waypoint 手里。围绕它的关键决策链——Rust 实现换取性能、xDS 传输协议搭配定制资源类型换取 10 倍配置效率、HBONE 复用标准 HTTP CONNECT 免去协议发明成本、策略反挂到工作负载换取低更新频率、证书代表工作负载身份换取节点级集中管理——每一环都在服务同一个目标:不破坏现有 Kubernetes 行为的前提下,把零信任加密的接入成本降到节点级。
延伸阅读与源码入口:
- 架构文档原文:ztunnel.md、Peer Authentication 实现、Ztunnel CNI 生命周期
- 配置协议定义:workload.proto、authorization.proto
- HBONE 协议库与示例:pkg/hbone(含
NewDialer客户端与NewServer服务端用法) - 部署物料:ztunnel Helm Chart
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00