首页
/ Kubernetes kube-proxy nftables 模式深度解析:netfilter 报文路径、规则链布局与第三方组件集成指南

Kubernetes kube-proxy nftables 模式深度解析:netfilter 报文路径、规则链布局与第三方组件集成指南

2026-09-06 19:16:47作者:牧宁李

导读

本文基于 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:

  • NFTablesNetlinkkube_features.go,v1.37 Beta、默认开启):让 kube-proxy 通过 netlink 而非调用 nft 二进制与内核交互;
  • KubeProxyNFTablesLocalhostNodePortskube_features.go):控制对 localhost 上 NodePort 流量的 userspace 代理与拒绝逻辑。

启用 nftables 模式的配置方式是在 KubeProxyConfiguration 中设置代理模式,其合法枚举值定义于 pkg/proxy/apis/config/types.go

ProxyModeNFTables ProxyMode = "nftables"

1.2 运行前提:内核与 nft 版本要求

nftables 后端不是开箱即用的,supported.go 中的 getNFTablesInterface 承担了环境校验职责:

  1. nft 二进制 >= 1.0.1:更早版本的 nft 无论是否只操作单张表,启动时都会尝试解析整个规则集,可能被其他组件在别的表里写入的新规则类型触发崩溃,导致 kube-proxy 被锁在自己的规则之外;
  2. 内核 >= 5.13:常量定义于 pkg/util/kernel/constants.goNFTablesKubeProxyKernelVersion = "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/ip6ipv4_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(preroutinginputforwardoutputpostrouting)上;
  • 依据报文流经的 hook,存在三条路径:
    • input 路径:宿主机网络命名空间内进程收包;报文来自 prerouting,路由决策后目的地址落在本机接口上则走 input
    • forward 路径:报文(典型如发往 Pod IP 的包)不发给本机而是继续转发,经 preroutingforwardpostrouting
    • output 路径:宿主机网络命名空间内进程发出的包,一律经过 output hook,之后若 DNAT 到宿主机端点会重新出现在 input 路径上。

这一模型决定了 kube-proxy 每一条规则应该挂在哪个 hook:DNAT 会影响后续路由方向,因此必须在路由决策之前的 prerouting/output 完成;MASQUERADE(源地址伪装)必须等最终路由确定出口网卡后才能执行,因此只能放在 postrouting

三、kube-proxy 对 nftables 的七类使用与 hook 布局

原 README 明确了 kube-proxy 用 nftables 干七件事,即所有规则的本质动机:

  1. DNAT 将发往 Service IP(ClusterIP、ExternalIP、LoadBalancer IP、节点 IP 上的 NodePort)的流量改写为对应 Endpoint IP;
  2. SNAT/MASQUERADE 在必要时伪装流量,保证回包会回到本节点/网络命名空间,从而能被正确“去 DNAT”;
  3. 丢弃LoadBalancerSourceRanges 过滤掉的报文;
  4. 丢弃 externalTrafficPolicy: Local 但本节点无本地 Endpoint 的 Service 流量;
  5. 拒绝(reject)完全无 Endpoint(无论本地或远端)的 Service 流量;
  6. 丢弃发往尚未分配的 ClusterIP 的报文;
  7. 拒绝发往 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 防火墙 preroutingoutput 低于(更紧急于)DNAT 链 源地址过滤必须先于 Service DNAT 发生
无 Endpoint Service 的 drop/reject inputforwardoutput(对 @no-endpoint-services);input(对 @no-endpoint-nodeports filter 内核 < 5.9 时 reject 不允许出现在 prerouting,需覆盖所有可能路径
MASQUERADE postrouting srcnat “masquerade” 语义即“伪装成报文将出去的接口的 IP”,须在最终路由决策之后
ClusterIP 未定义端口 → reject;未分配 ClusterIP → drop forwardoutput 高于(更不紧急于)DNAT 链 保证合法流量已被 DNAT,落网的才需要拒绝/丢弃

其中针对 ClusterIP 的 reject 规则使用 @cluster-ips 集合匹配(合法 ClusterIP 的非法端口 → reject),而 drop 规则匹配所有属于 ServiceCIDR 的地址(未分配的 ClusterIP → drop)。注意最后一条 drop 规则仅在启用 MultiCIDRServiceAllocator feature 时才安装——这正是 setupNFTablesif 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-checkfilter-inputfilter-forwardfilter-output 三处进入,nodeport-endpoints-check 只从 filter-input 进入——因为 reject 动作在 5.9 之前的 prerouting hook 上非法,无法像防火墙那样挂到 pre-DNAT 链;
  • 无 Endpoint 判断与防火墙判断都带 ct state new 条件(只处理新建连接,已建立的连接直接放行以保持转发路径精简)。

4.3 集合与映射清单

setupNFTables 中创建的对象(proxier.go),与 Proxier 结构体里的 8 个 nftElementStorageproxier.go)一一对应,其中:

  • @cluster-ipsset,类型 ipv4_addr/ipv6_addr):记录当前活跃的全部 ClusterIP;
  • @service-ipsmap,键 IP . inet_proto . inet_service,值 verdict):ClusterIP/ExternalIP/LoadBalancer IP 的(IP,协议,端口)→ 跳转动作分发表;
  • @service-nodeportsmap,键 inet_proto . inet_service):NodePort → 跳转动作分发表;
  • @nodeport-ipsset):接受 NodePort 流量的节点 IP。若 NodePortAddresses 匹配所有地址则直接删除该 set(匹配逻辑改为 fib daddr type local + 排除 localhost),否则写入具体节点 IP;
  • @firewall-ipsmap):受 LoadBalancerSourceRanges 约束的目的地址(LB IP,协议,端口)→ verdict;
  • @no-endpoint-servicesmap,键含 IP)与 @no-endpoint-nodeportsmap,键不含 IP):无 Endpoint Service/NodePort → drop/reject verdict;
  • @hairpin-connectionsset,键 addr . addr . inet_proto . inet_service):宿主机网络命名空间内的 hairpin(发夹)连接;
  • @cluster-ips-check 链内的 reject/drop 规则即“第七件事”的实现;其中 reject 匹配 ip daddr @cluster-ips set,drop 匹配 ServiceCIDR 集合。

每个 nftElementStorageproxier.go)以“key/value 字符串 map + leftoverKeys 差集”的方式做增量管理:readOrReset 在整表重建时读取内核现有元素;ensureElem 只对不存在的元素发出 add(值变化则先删后加);cleanupLeftoverKeys 在本轮同步结束后删除不再被引用的残留元素,实现“全量对账、增量写入”。

4.4 事务化同步:syncProxyRules

对 nftables 的所有写操作都通过事务(knftables.Transaction)提交。核心函数 syncProxyRules 的执行要点:

  1. 在收到完整的 Service 与 EndpointSlice 之前不会同步(isInitialized,防止重启后以半份数据覆盖规则);
  2. BoundedFrequencyRunnerproxier.go)节流调度,支持全量同步(setupNFTables + 对账)与基于变更跟踪器的部分同步
  3. 旧 Service/Endpoint 遗留的链先 flush 并登记到 staleChains,等一秒钟后内核确认无引用再删除(proxier.go)。原 README 未展开的这个细节是内核 < 6.2 的已知行为:删除 map 元素后内核需短暂时间才能意识到链已无引用,立即删除链会失败;
  4. 同步成功后按需清理 UDP Service 的过期 conntrack 条目(proxier.go),并上报 SyncProxyRulesLatencyNFTablesSyncFailuresTotal 等指标。

五、规则级实现细节:从 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 创建独立的带超时的动态 setaffinity-*,带 dynamictimeout 标志,超时 = 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 natpriority dstnathook output(output 路径)或 hook prerouting(input/forward 路径)的链上完成。因此,优先值早于 dstnat 的其他表链看到的还是发往 Service IP 的流量;晚于 dstnat 的链看到的已是发往 Endpoint IP 的流量
  • kube-proxy 的 MASQUERADE 在 type nathook postroutingpriority srcnat 的链上完成。早于 srcnat 运行的链始终能看到原始客户端 IP,晚于它运行的链则对部分流量看到伪装后的源 IP;
  • 无 Endpoint Service 的 drop/reject 发生在 type filterpriority 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_totalnftables_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.gocleanup_test.go)之中。

登录后查看全文
热门项目推荐
相关项目推荐