Kubernetes kube-proxy nftables 模式深度解析:netfilter 报文路径、规则链布局与第三方组件集成指南
导读
本文基于 Kubernetes 官方仓库中 pkg/proxy/nftables/README.md 及其对应实现源码,系统讲解 kube-proxy 基于 nftables API(内核 netfilter 子系统)实现 Service 代理的完整原理与落地细节。你将掌握 netfilter 的输入/转发/输出三类报文路径、kube-proxy 在七个 hook 上承担的具体职责、内部表与链的组织方式,以及 CNI、NetworkPolicy、Service Mesh 等组件如何在不破坏 kube-proxy 规则的前提下正确与其协同工作。
一、背景:kube-proxy 的 nftables 后端
Kubernetes 的 kube-proxy 组件负责把集群中的 Service(ClusterIP、ExternalIP、LoadBalancer IP、NodePort)流量代理到后端 Endpoint。历史上它主要通过 iptables(netfilter 的 ip_tables 接口)实现,而 nftables 是内核 netfilter 子系统新一代的规则管理框架。
本仓库中的 nftables 实现位于 pkg/proxy/nftables/,包含 proxier.go(核心实现)、supported.go(运行环境检测)、cleanup.go(残留规则清理)、doc.go(非 Linux 平台测试占位)与对应的单元测试。
1.1 功能开关与版本演进
从当前仓库的 pkg/features/kube_features.go 可以看到 NFTablesProxyMode feature gate 的演进:
- v1.29:Alpha,默认关闭;
- v1.31:Beta,默认开启;
- v1.33:GA,默认开启且锁定(
LockToDefault: true,无法再显式关闭)。
此外还有两个相关 feature gate:
NFTablesNetlink(kube_features.go,v1.37 Beta、默认开启):让 kube-proxy 通过 netlink 而非调用nft二进制与内核交互;KubeProxyNFTablesLocalhostNodePorts(kube_features.go):控制对 localhost 上 NodePort 流量的 userspace 代理与拒绝逻辑。
启用 nftables 模式的配置方式是在 KubeProxyConfiguration 中设置代理模式,其合法枚举值定义于 pkg/proxy/apis/config/types.go:
ProxyModeNFTables ProxyMode = "nftables"
1.2 运行前提:内核与 nft 版本要求
nftables 后端不是开箱即用的,supported.go 中的 getNFTablesInterface 承担了环境校验职责:
- nft 二进制 >= 1.0.1:更早版本的
nft无论是否只操作单张表,启动时都会尝试解析整个规则集,可能被其他组件在别的表里写入的新规则类型触发崩溃,导致 kube-proxy 被锁在自己的规则之外; - 内核 >= 5.13:常量定义于 pkg/util/kernel/constants.go(
NFTablesKubeProxyKernelVersion = "5.13")。其隐含假设是发行版提供的 nft 二进制与内核特性同步(见 kubernetes#122743 的背景讨论)。若你的内核版本较新但 nft 较老,可能出现 kube-proxy 使用新语法而系统 nft 无法解析的互害场景。
如需跳过内核版本检查,可设置环境变量 KUBE_PROXY_NFTABLES_SKIP_KERNEL_VERSION_CHECK 为非空值。
1.3 单栈与双栈
代码采用“每个 IP 族一张独立规则表 + MetaProxier 聚合”的方式支持双栈。NewDualStackProxier 会分别为 IPv4、IPv6 创建一个 Proxier 实例(两者使用相同的表名 kube-proxy,但分属 ip/ip6 族),再用 metaproxier.NewMetaProxier 组合。每个 Proxier 有独立的 ipFamily 字段(proxier.go),在生成规则时选择 ip/ip6、ipv4_addr/ipv6_addr 等关键字。
二、netfilter 通用原理:五类 hook 与三条报文路径
理解 nftables kube-proxy 前,必须先掌握 netfilter 的报文处理模型。原 README 给出了如下示意图(为便于阅读重新排版):
+================+ +=====================+
| hostNetwork IP | | hostNetwork process |
+================+ +=====================+
^ |
- - - - - - - - | - - - - - [*] - - - - - - - - -
| v
+-------+ +--------+
| input | | output |
+-------+ +--------+
^ |
+------------+ | +---------+ v +-------------+
| prerouting |-[*]-+-->| forward |--+-[*]->| postrouting |
+------------+ +---------+ +-------------+
^ |
- - - - | - - - - - - - - - - - - - - | - - - -
| v
+---------+ +--------+
--->| ingress | | egress |--->
+---------+ +--------+
图中 [*] 代表一次路由决策,顶部两框之外的所有方框都是 netfilter hook。几个必须澄清的要点(原文档特别强调):
- 标准版本示意图常把顶部两框合并为“local process”,容易让人误以为一个报文可以依次经过
input→ “本地进程” →output,事实上这是不可能的; ingress/egress两个 hook 较为特殊,基本不对 kube-proxy 开放——kube-proxy 只活跃在下半部分五个主 hook(prerouting、input、forward、output、postrouting)上;- 依据报文流经的 hook,存在三条路径:
- input 路径:宿主机网络命名空间内进程收包;报文来自
prerouting,路由决策后目的地址落在本机接口上则走input; - forward 路径:报文(典型如发往 Pod IP 的包)不发给本机而是继续转发,经
prerouting→forward→postrouting; - output 路径:宿主机网络命名空间内进程发出的包,一律经过
outputhook,之后若 DNAT 到宿主机端点会重新出现在input路径上。
- input 路径:宿主机网络命名空间内进程收包;报文来自
这一模型决定了 kube-proxy 每一条规则应该挂在哪个 hook:DNAT 会影响后续路由方向,因此必须在路由决策之前的 prerouting/output 完成;MASQUERADE(源地址伪装)必须等最终路由确定出口网卡后才能执行,因此只能放在 postrouting。
三、kube-proxy 对 nftables 的七类使用与 hook 布局
原 README 明确了 kube-proxy 用 nftables 干七件事,即所有规则的本质动机:
- 用 DNAT 将发往 Service IP(ClusterIP、ExternalIP、LoadBalancer IP、节点 IP 上的 NodePort)的流量改写为对应 Endpoint IP;
- 用 SNAT/MASQUERADE 在必要时伪装流量,保证回包会回到本节点/网络命名空间,从而能被正确“去 DNAT”;
- 丢弃被
LoadBalancerSourceRanges过滤掉的报文; - 丢弃
externalTrafficPolicy: Local但本节点无本地 Endpoint 的 Service 流量; - 拒绝(reject)完全无 Endpoint(无论本地或远端)的 Service 流量;
- 丢弃发往尚未分配的 ClusterIP 的报文;
- 拒绝发往 ClusterIP 上未定义端口(合法 IP、非法端口)的报文。
3.1 规则在 hook 上的布局
原 README 给出的规则布局如下,每条都与 setupNFTables 中的实际代码一一对应:
| 动作 | 挂载点 | 优先级 | 原因 |
|---|---|---|---|
| 入站 DNAT(外部→各类 Service IP、Pod→各类 Service IP) | prerouting |
dstnat | 必须在路由决策前完成,因为 Endpoint IP 的选择会影响报文随后走 input 还是 forward 路径 |
| 出站 DNAT(宿主机进程→各类 Service IP) | output |
dstnat | 宿主机进程流量一定走 output 路径 |
| LoadBalancerSourceRanges 防火墙 | prerouting 与 output |
低于(更紧急于)DNAT 链 | 源地址过滤必须先于 Service DNAT 发生 |
| 无 Endpoint Service 的 drop/reject | input、forward、output(对 @no-endpoint-services);input(对 @no-endpoint-nodeports) |
filter | 内核 < 5.9 时 reject 不允许出现在 prerouting,需覆盖所有可能路径 |
| MASQUERADE | postrouting |
srcnat | “masquerade” 语义即“伪装成报文将出去的接口的 IP”,须在最终路由决策之后 |
| ClusterIP 未定义端口 → reject;未分配 ClusterIP → drop | forward 与 output |
高于(更不紧急于)DNAT 链 | 保证合法流量已被 DNAT,落网的才需要拒绝/丢弃 |
其中针对 ClusterIP 的 reject 规则使用 @cluster-ips 集合匹配(合法 ClusterIP 的非法端口 → reject),而 drop 规则匹配所有属于 ServiceCIDR 的地址(未分配的 ClusterIP → drop)。注意最后一条 drop 规则仅在启用 MultiCIDRServiceAllocator feature 时才安装——这正是 setupNFTables 中 if len(proxier.serviceCIDRs) > 0 的条件分支,ServiceCIDR 列表由 OnServiceCIDRsChanged 按 IP 族过滤后维护。
四、单张表里的完整世界:链、集与映射
nftables 模式与前代 iptables 实现的一大差异是对象组织方式:kube-proxy 的全部规则都在一张表内(表名常量 kubeProxyTable = "kube-proxy",见 proxier.go),其后缀名没有 kube-/kube-proxy- 前缀也无需再有——表本身已界定了命名空间。对象类型分为三类:
- 基链(base chains):直接挂在 netfilter hook 上;
- 普通链(regular chains):只能被规则
jump/goto进入; - 集合(sets)与映射(maps):以 O(1) 查表方式替代 iptables 时代一长串顺序匹配的规则。
4.1 基链清单
定义于 nftablesBaseChains,命名统一为 ${type}-${hook}:
| 链名 | 类型 | hook | 优先级 |
|---|---|---|---|
| filter-prerouting-pre-dnat | filter | prerouting | dstnat - 10(早于 DNAT,用于源范围防火墙) |
| filter-output-pre-dnat | filter | output | dstnat - 10 |
| filter-input | filter | input | filter |
| filter-forward | filter | forward | filter |
| filter-output | filter | output | filter |
| nat-prerouting | nat | prerouting | dstnat |
| nat-output | nat | output | dstnat |
| nat-postrouting | nat | postrouting | srcnat |
4.2 顶层跳转链(jump chains)
nftablesJumpChains 定义了基链到普通链的跳转拓扑。它们体现了两条原 README 强调的约束:
service-endpoints-check从filter-input、filter-forward、filter-output三处进入,nodeport-endpoints-check只从filter-input进入——因为 reject 动作在 5.9 之前的preroutinghook 上非法,无法像防火墙那样挂到 pre-DNAT 链;- 无 Endpoint 判断与防火墙判断都带
ct state new条件(只处理新建连接,已建立的连接直接放行以保持转发路径精简)。
4.3 集合与映射清单
setupNFTables 中创建的对象(proxier.go),与 Proxier 结构体里的 8 个 nftElementStorage(proxier.go)一一对应,其中:
@cluster-ips(set,类型ipv4_addr/ipv6_addr):记录当前活跃的全部 ClusterIP;@service-ips(map,键IP . inet_proto . inet_service,值 verdict):ClusterIP/ExternalIP/LoadBalancer IP 的(IP,协议,端口)→ 跳转动作分发表;@service-nodeports(map,键inet_proto . inet_service):NodePort → 跳转动作分发表;@nodeport-ips(set):接受 NodePort 流量的节点 IP。若NodePortAddresses匹配所有地址则直接删除该 set(匹配逻辑改为fib daddr type local+ 排除 localhost),否则写入具体节点 IP;@firewall-ips(map):受 LoadBalancerSourceRanges 约束的目的地址(LB IP,协议,端口)→ verdict;@no-endpoint-services(map,键含 IP)与@no-endpoint-nodeports(map,键不含 IP):无 Endpoint Service/NodePort → drop/reject verdict;@hairpin-connections(set,键addr . addr . inet_proto . inet_service):宿主机网络命名空间内的 hairpin(发夹)连接;@cluster-ips-check链内的 reject/drop 规则即“第七件事”的实现;其中 reject 匹配ip daddr @cluster-ips set,drop 匹配 ServiceCIDR 集合。
每个 nftElementStorage(proxier.go)以“key/value 字符串 map + leftoverKeys 差集”的方式做增量管理:readOrReset 在整表重建时读取内核现有元素;ensureElem 只对不存在的元素发出 add(值变化则先删后加);cleanupLeftoverKeys 在本轮同步结束后删除不再被引用的残留元素,实现“全量对账、增量写入”。
4.4 事务化同步:syncProxyRules
对 nftables 的所有写操作都通过事务(knftables.Transaction)提交。核心函数 syncProxyRules 的执行要点:
- 在收到完整的 Service 与 EndpointSlice 之前不会同步(
isInitialized,防止重启后以半份数据覆盖规则); - 由
BoundedFrequencyRunner(proxier.go)节流调度,支持全量同步(setupNFTables+ 对账)与基于变更跟踪器的部分同步; - 旧 Service/Endpoint 遗留的链先
flush并登记到staleChains,等一秒钟后内核确认无引用再删除(proxier.go)。原 README 未展开的这个细节是内核 < 6.2 的已知行为:删除 map 元素后内核需短暂时间才能意识到链已无引用,立即删除链会失败; - 同步成功后按需清理 UDP Service 的过期 conntrack 条目(proxier.go),并上报
SyncProxyRulesLatency、NFTablesSyncFailuresTotal等指标。
五、规则级实现细节:从 Service 到 Endpoint 的落盘
5.1 MASQUERADE 标记机制
与 iptables 版本类似,nftables 版本使用**包标记(mark)**配合 MASQUERADE:NewProxier 中由 config.NFTables.MasqueradeBit 决定 mark 位(proxier.go),生成形如 mark set mark or 0x00004000 的规则(masqueradeRule)。masquerading 链(挂在 nat-postrouting,见 proxier.go)中:
- 命中该 mark 位的包执行
mark xor mark(清零)后masquerade fully-random; - 命中
@hairpin-connections集合的四元组(源IP.目的IP.协议.目的端口)也执行masquerade fully-random——当 Pod 访问本节点上同 Pod 所属 Service 时,必须伪装源地址才能让回包正确回到 Pod(hairpin 场景); masqueradeAll为真时,所有发往 ClusterIP 的流量在services链中直接打 mark;否则仅对“来自集群外、非本机”的 ClusterIP 流量打 mark(masquerade clusterIP traffic from outside cluster),这正是本地流量保留源 IP 的基础。
值得注意的是:使用 mark 位表示“需要被伪装”并非 kube-proxy 的公共 API,第三方组件不应试图通过设置/清除某个特定 mark 位来让流量被伪装或不被伪装(原 README 在集成一节明确警告,下文详述)。
5.2 Service 分发链与 DNAT
services 链按以下顺序处理:先命中 @service-ips map(ClusterIP/ExternalIP/LB IP 流量),再命中 @service-nodeports map(NodePort 流量,NodePort 不受 LoadBalancerSourceRanges 约束)。所有具有 Endpoint 的 Service 都跳转到 per-service 的“外部目标链”(external chain),而外部目标链内部又按流量策略细分:
externalTrafficPolicy: Cluster:直接对全部流量 masquerade,然后跳转到 cluster-policy 链做 DNAT;externalTrafficPolicy: Local:先为“本机发起的 Pod 流量”与“fib saddr type local的本机流量”做短路(short-circuit)跳转到 cluster-policy 链,其余流量才进入只含本机 Endpoint 的 local-policy 链,从而保留源 IP 直达本机后端(源码见 proxier.go 及其规则注释)。
Endpoint 的负载均衡(writeServiceToEndpointDNATs)利用 nftables 的 numgen 随机数表达式:
- 单 Endpoint:直接写一条普通
dnat to IP:PORT,避免构造 map(向 nftables 插入 map 是 O(n) 甚至更差的操作,单端点场景纯属浪费); - 多 Endpoint:生成
dnat ip addr . port to numgen random mod N map { 0:ep1, 1:ep2, ... },由内核在数据面完成随机负载均衡,而非像老版本 iptables 那样依赖概率匹配的规则序列。
5.3 Session Affinity(会话保持)
当 Service 使用 sessionAffinity: ClientIP 时(proxier.go),kube-proxy 为每个 Endpoint 创建独立的带超时的动态 set(affinity-*,带 dynamic、timeout 标志,超时 = StickyMaxAgeSeconds),在 endpoint 链中先用 update @<affinity-set> { ip saddr } 把来源 IP 记入会话,再执行 DNAT;分发则改为按“来源 IP 是否命中某 affinity set”优先跳转(writeServiceToEndpointJumps),未命中时再落入 numgen random mod N vmap 做随机选择。
5.4 链名生成与长度约束
nftables 对链名有严格约束(字符集与长度上限)。hashAndTruncate 用 SHA-256 + Base32 前 8 位做前缀再截断,既保证唯一性又保持名字可读。形如 HASH-namespace/service/protocol/portName(例 ULMVA6XW-ns1/svc1/tcp/p80);IPv6 Endpoint 中的冒号会被替换成点以符合链名字符集。注释会经由 truncateNFTablesComment 截断到 nftables 注释长度上限(注释仅用于可读性,不影响转发)。
六、与第三方组件的集成:一份“低层互操作”指南
原 README 用一整节说明 “Integrating with kube-proxy's nftables mode”——这面向的是 Pod 网络(CNI)、NetworkPolicy、Service Mesh 等同样操作 netfilter 的实现,核心规则如下:
铁律一:绝不触碰 kube-proxy 表。 除 kube-proxy 之外的任何组件都永远不要修改 kube-proxy nftables 表及其内部任何链、集、映射。每个组件都应创建自己的表,只在自己的表内工作。
铁律二:用基链优先级排序先后。 通过在自有表基链上设置合适的 priority,可以确定自己的规则运行在 kube-proxy 规则之前还是之后:
- kube-proxy 的 Service DNAT 在
type nat、priority dstnat、hook output(output 路径)或hook prerouting(input/forward 路径)的链上完成。因此,优先值早于 dstnat 的其他表链看到的还是发往 Service IP 的流量;晚于 dstnat 的链看到的已是发往 Endpoint IP 的流量; - kube-proxy 的 MASQUERADE 在
type nat、hook postrouting、priority srcnat的链上完成。早于 srcnat 运行的链始终能看到原始客户端 IP,晚于它运行的链则对部分流量看到伪装后的源 IP; - 无 Endpoint Service 的 drop/reject 发生在
type filter、priority filter,hook 为input/output/forward的链中。
铁律三:mark 位不是公共 API。 如 5.1 节所述,不能通过设置或清除某个特定 mark 位来操纵流量是否被伪装。
集成实践启示:Service Mesh 若想“先看 Service IP 再做七层路由、再放行回程”,可把链放在 dstnat 之前;NetworkPolicy 若想对“已解出真实 Endpoint 后的流量”做二次过滤,则可放在 dstnat 之后。这一契约是原 README 对外部生态最重要的交付物。
七、运维与故障排查
7.1 查看当前规则
事务失败时 kube-proxy 会在日志(V(4),且受 24 小时一次的限速器约束)中打印失败事务与整表内容(logFailure 通过执行 nft list table kube-proxy 抓取)。日常排查可手动执行:
# 查看整张 kube-proxy 表(按 IP 族区分)
nft list table ip kube-proxy
nft list table ip6 kube-proxy
# 只查看某个映射/集合的元素
nft list map ip kube-proxy service-ips
7.2 清理遗留规则
切换代理模式或节点退役时,可用 CleanupLeftovers 删除 kube-proxy 在 ip/ip6 两族创建的表(其事务对 knftables.Table{} 执行 delete,并容忍 NotFound 错误)。前代 iptables 模式同样提供了对应的清理逻辑,二者互不干扰。
7.3 同步模型与可观测性
- 对象变更事件(
OnServiceUpdate/OnEndpointSliceUpdate等)会触发Sync(),由syncRunner合并节流,避免抖动风暴;首次全量同步发生在 Service 与 EndpointSlice 都同步完成后; - 同步成功后可观察
sync_proxy_rules_last_timestamp,失败可观察nftables_sync_failures_total与nftables_cleanup_failures_total(按 IP 族打标签),网络编程时延见network_programming_duration——这些指标名与语义均可从 pkg/proxy/metrics 对应源码验证。
结语
kube-proxy 的 nftables 后端把“Service IP → Endpoint IP”的转发语义映射到了内核 netfilter 的五类 hook 上:prerouting/output 做 DNAT,postrouting 做 MASQUERADE,input/forward/output 处理无 Endpoint 拒绝与未分配 ClusterIP 的丢弃,pre-DNAT 链承载源地址防火墙。通过“单表 + 事务 + 集合/映射 + numgen 负载均衡”的现代设计,它在规则规模与同步效率上相较 iptables 长链方式有本质差异。无论你是想深入内核数据面的开发者,还是需要与 kube-proxy 在同一节点上协同的 CNI/Service Mesh 实现者,本文梳理的 hook 布局、优先级契约与表结构都能作为可靠的实现依据——而最权威的细节始终在仓库的 pkg/proxy/nftables/ 源码与其测试用例(proxier_test.go、cleanup_test.go)之中。
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 StartedRust0624
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