Kubernetes SNAT 三种场景:从 hairpin 到 NodePort 的源地址改写¶
文章摘要¶
kube-proxy 的 iptables 规则主要做 DNAT(改目标 IP),但有三类场景必须同时做 SNAT(改源 IP)。这篇文章拆解三种 SNAT 的触发条件、iptables 规则链、conntrack 证据,以及 externalTrafficPolicy 对 NodePort 流量的影响。
一、为什么 DNAT 不够用¶
kube-proxy 的核心工作是 DNAT——把目标 IP 从 ClusterIP 改写成后端 Pod IP。大部分场景下 DNAT 就够了:Pod 发请求到 ClusterIP,iptables DNAT 改目标地址,包到达后端 Pod,回包原路返回,conntrack 反向 NAT 把源 IP 改回 ClusterIP。整个过程对 Pod 透明。
但有三种场景,只做 DNAT 会出问题:
- Pod 访问 Service,DNAT 选中了自己——包变成自己访问自己,回包走 loopback,conntrack 记录对不上
- 节点上的进程访问 Service——回包直接走本地路由到节点,绕过 conntrack 反向 NAT,源 IP 是 Pod IP 而不是 ClusterIP
- 外部流量通过 NodePort 进入——回包如果走 SNAT,源 IP 变成节点 IP,后端 Pod 看不到真实客户端 IP
三种场景,三种 SNAT 策略,对应不同的 iptables 规则链。这篇文章逐一拆解。
前置阅读
- Kube-Proxy 工作原理——iptables 三级跳转链(KUBE-SERVICES → KUBE-SVC → KUBE-SEP)、DNAT 机制
- 一行 curl 的完整旅程——hairpin NAT 的完整实测数据(strace + conntrack)
二、SNAT 的 iptables 基础设施:KUBE-MARK-MASQ¶
在拆具体场景之前,先理解 kube-proxy 做 SNAT 的通用机制。
kube-proxy 不会在每条 KUBE-SEP 链里直接写 SNAT 规则。它用了一个更聪明的做法——打标记,统一处理。
2.1 KUBE-MARK-MASQ 链¶
输出:
Chain KUBE-MARK-MASQ (0 references)
target prot opt source destination
MARK all -- 0.0.0.0/0 0.0.0.0/0 MARK or 0x4000
这条链只做一件事:给包打上 0x4000 标记。它本身不做 SNAT,只是一个"待会要做 MASQUERADE"的标记。
2.2 POSTROUTING 链统一处理¶
输出:
-N KUBE-POSTROUTING
-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN
-A KUBE-POSTROUTING -j MARK --set-xmark 0x4000/0x0
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE --random-fully
三条规则,逻辑很清晰:
- 没有
0x4000标记 → RETURN(跳过,不做 SNAT) - 有标记 → 清除标记(
set-xmark 0x4000/0x0把标记位清零,避免重复处理) - → MASQUERADE(做 SNAT,源 IP 改成出接口 IP)
所以 kube-proxy 的 SNAT 机制是两步走:
flowchart LR
A["数据包进入<br/>iptables"] --> B["KUBE-SEP 链<br/>DNAT 改目标 IP"]
B --> C{"需要 SNAT 吗?"}
C -->|"是"| D["KUBE-MARK-MASQ<br/>打标记 0x4000"]
C -->|"否"| E["不打标记"]
D --> F["POSTROUTING<br/>KUBE-POSTROUTING"]
E --> F
F --> G{"mark 0x4000?"}
G -->|"是"| H["MASQUERADE<br/>SNAT 改源 IP"]
G -->|"否"| I["直接放行"]
classDef mark fill:#FEF3C7,stroke:#D97706,color:#0F172A
classDef masq fill:#FCE7F3,stroke:#DB2777,color:#0F172A
classDef normal fill:#F1F5F9,stroke:#94A3B8,color:#0F172A
class D,H mark
class B masq
class A,C,E,F,G,I normal 这个设计的好处是:KUBE-SEP 链只管 DNAT + 打标记,SNAT 统一在 POSTROUTING 处理。规则数量少,维护简单。
2.3 什么时候打标记¶
kube-proxy 在两个位置放打标记规则,分别覆盖两个 SNAT 场景。以 nginx-test Service(ClusterIP 10.100.241.29)为例:
位置一:KUBE-SVC 链第一条规则
输出:
-N KUBE-SVC-W67AXLFK7VEUVN6G
-A KUBE-SVC-W67AXLFK7VEUVN6G ! -s 10.244.0.0/16 -d 10.100.241.29/32 -p tcp -m comment --comment "default/nginx-test cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
-A KUBE-SVC-W67AXLFK7VEUVN6G -m comment --comment "default/nginx-test -> 10.244.19.69" -m statistic --mode random --probability 0.333 -j KUBE-SEP-6PDWQRDBUEP5HVBT
-A KUBE-SVC-W67AXLFK7VEUVN6G -m comment --comment "default/nginx-test -> 10.244.30.120" -m statistic --mode random --probability 0.500 -j KUBE-SEP-FHKNY3MFLBZWZGVU
-A KUBE-SVC-W67AXLFK7VEUVN6G -m comment --comment "default/nginx-test -> 10.244.5.12" -j KUBE-SEP-OO34H6TCD4WVYNCR
第一条规则的关键条件是 ! -s 10.244.0.0/16——源 IP 不在 Pod 网段时才打标记。这精确命中了"节点本机进程访问 Service"的场景:节点 IP(如 192.168.114.148)不在 10.244.0.0/16 里,会匹配这条规则被打标记。而 Pod 流量的源 IP 在 10.244.0.0/16 里,不匹配,直接跳过。
位置二:KUBE-SEP 链第一条规则
输出:
-N KUBE-SEP-OO34H6TCD4WVYNCR
-A KUBE-SEP-OO34H6TCD4WVYNCR -s 10.244.5.12/32 -m comment --comment "default/nginx-test" -j KUBE-MARK-MASQ
-A KUBE-SEP-OO34H6TCD4WVYNCR -p tcp -m comment --comment "default/nginx-test" -m tcp -j DNAT --to-destination 10.244.5.12:80
第一条规则:-s 10.244.5.12/32 -j KUBE-MARK-MASQ——如果包的源 IP 等于这个后端 Pod IP,打标记。这就是 hairpin 的触发条件:Pod 访问 Service,DNAT 选中了自己。
第二条规则才是 DNAT:-j DNAT --to-destination 10.244.5.12:80。
注意打标记在 DNAT 之前执行——此时包还是 src=10.244.5.12, dst=10.100.241.29,源 IP 还是原始发起方。打完标记后 DNAT 把 dst 改成 10.244.5.12,到 POSTROUTING 时发现带了 0x4000 标记,做 MASQUERADE。
两层规则形成精确的 SNAT 触发矩阵:
| 规则位置 | 触发条件 | 命中场景 | Pod 跨节点访问 Service |
|---|---|---|---|
| KUBE-SVC 第一条 | ! -s 10.244.0.0/16 | 节点本机访问 Service | 不命中(源在 Pod 网段) |
| KUBE-SEP 第一条 | -s <后端PodIP> | Hairpin(选中自己) | 不命中(源 ≠ 目标 Pod IP) |
Pod 正常访问跨节点 Service 时,两条规则都不命中——KUBE-SVC 的 ! -s 10.244.0.0/16 不匹配(源在 Pod 网段),KUBE-SEP 的 -s <PodIP> 也不匹配(源 IP 是发起方 Pod IP,不等于后端 Pod IP)。所以不做 SNAT,源 IP 保持 Pod IP 不变。
下面逐一拆解三个场景。
三、场景一:Hairpin NAT(Pod 访问自己后面的 Service)¶
3.1 问题¶
Pod 10.244.5.12 访问 Service nginx-test(ClusterIP 10.100.241.29),iptables DNAT 随机选中了 10.244.5.12 自己作为后端。如果只做 DNAT 不做 SNAT:
- 包变成
src=10.244.5.12, dst=10.244.5.12(自己访问自己) - 内核看到 dst 是自己的 IP,走 loopback
- 回包
src=10.244.5.12, dst=10.244.5.12,直接走本地,不经过 iptables - conntrack 记录的 ORIGINAL 方向是
dst=10.100.241.29,回包的dst=10.244.5.12对不上 - conntrack 反向 NAT 失败,Pod 收到的包源 IP 是 Pod IP 而不是 ClusterIP
3.2 解决方案¶
kube-proxy 在 KUBE-SEP 链里加了一条规则:如果包的源 IP 等于这个后端 Pod IP,打 KUBE-MARK-MASQ 标记。
输出:
-N KUBE-SEP-OO34H6TCD4WVYNCR
-A KUBE-SEP-OO34H6TCD4WVYNCR -s 10.244.5.12/32 -m comment --comment "default/nginx-test" -j KUBE-MARK-MASQ
-A KUBE-SEP-OO34H6TCD4WVYNCR -p tcp -m comment --comment "default/nginx-test" -m tcp -j DNAT --to-destination 10.244.5.12:80
iptables 规则的执行顺序:
- PREROUTING → KUBE-SERVICES:匹配 ClusterIP
10.100.241.29:80,跳到 KUBE-SVC - KUBE-SVC:statistic 随机选后端,跳到 KUBE-SEP-OO34H6TCD4WVYNCR
- KUBE-SEP:
-s 10.244.5.12/32 -j KUBE-MARK-MASQ——如果源 IP 是10.244.5.12(自己),打标记-j DNAT --to-destination 10.244.5.12:80——DNAT 改目标 IP
打标记发生在 DNAT 之前——此时包还是 src=10.244.5.12, dst=10.100.241.29。打完标记后 DNAT 把 dst 改成 10.244.5.12,到 POSTROUTING 时发现带了 0x4000 标记,做 MASQUERADE 把 src 改成节点 IP 192.168.114.148。
3.3 conntrack 证据¶
在 curl 全链路文章中已经实测过 hairpin NAT 的 conntrack 条目:
src=10.244.5.12 dst=10.100.241.29 sport=34188 dport=80
src=10.244.5.12 dst=192.168.114.148 sport=80 dport=34188 [ASSURED]
ORIGINAL 方向:src=Pod, dst=ClusterIP REPLY 方向:src=Pod:80, dst=节点IP——注意这里的 dst 是节点 IP 而不是 Pod IP,说明回包经过了 SNAT(conntrack 的 REPLY 方向记录的是 NAT 改写后的真实地址,详见 第六节的解释)。
详细实测数据
hairpin NAT 的完整 strace + conntrack 实测数据见 一行 curl 的完整旅程,conntrack -L 的条目字段与 ORIGINAL/REPLY 判读见 conntrack 命令详解,这里不重复展开。
四、场景二:节点访问 Service¶
4.1 问题¶
worker01 上的进程(不是 Pod,是节点本身)执行 curl 10.100.241.29:80。包从节点的 OUTPUT 链进入 iptables:
- PREROUTING 不经过(包不是从外部接口进来的,是本机产生的)
- OUTPUT → KUBE-SERVICES → KUBE-SVC → KUBE-SEP:DNAT 改 dst 为后端 Pod IP
- 假设选中
10.244.19.69(worker03 上的 Pod)
如果只做 DNAT 不做 SNAT,包变成 src=192.168.114.148, dst=10.244.19.69(假设选中 worker03 上的 Pod),经过 Calico IPIP 隧道到达 worker03,nginx Pod 回包 src=10.244.19.69, dst=192.168.114.148,原路返回。
看起来能通?但如果不做 SNAT,后端 Pod 看到的源 IP 是节点 IP,而不是 Pod IP。这在某些场景下会导致问题——比如后端 Pod 做源 IP 白名单校验时,期望看到 Pod 网段的地址。
更重要的是,kube-proxy 的设计逻辑是:节点本机流量不属于 Pod 网段,需要做 SNAT 统一处理。这不是"不做也行"的可选项,而是 kube-proxy 主动为之的策略。
4.2 规则分析¶
节点本机进程访问 Service 时,包从 OUTPUT 链进入 KUBE-SERVICES。KUBE-SERVICES 没有通用的 ClusterIP 范围打标记规则,它只是逐条匹配具体 Service,跳到对应的 KUBE-SVC 链。
打标记发生在 KUBE-SVC 链的第一条规则:
-A KUBE-SVC-W67AXLFK7VEUVN6G ! -s 10.244.0.0/16 -d 10.100.241.29/32 -p tcp --dport 80 -j KUBE-MARK-MASQ
条件是 ! -s 10.244.0.0/16——源 IP 不在 Pod 网段。节点本机进程的源 IP 是物理 IP(如 192.168.114.145)或 tunl0 IP,不在 10.244.0.0/16 里,所以匹配这条规则,被打上 MASQ 标记。
而 Pod 流量的源 IP 在 10.244.0.0/16 里,不匹配这条规则,不会被打标记。这就是为什么 Pod 访问 Service 不做 SNAT、节点访问 Service 做 SNAT——区分逻辑就在 ! -s 10.244.0.0/16 这一个条件里。
4.3 实测:master01 访问 nginx-test¶
在 master01(192.168.114.145)上执行:
conntrack 输出:
tcp 6 112 TIME_WAIT src=192.168.114.145 dst=10.100.241.29 sport=44302 dport=80
[ASSURED] src=10.244.5.12 dst=10.244.241.64 sport=80 dport=21506 use=1
- ORIGINAL:
src=192.168.114.145, dst=10.100.241.29(master01 → ClusterIP) - REPLY:
src=10.244.5.12, dst=10.244.241.64(nginx Pod → ?)
REPLY 方向的 dst=10.244.241.64 不是 master01 的物理 IP(192.168.114.145),而是 master01 的 tunl0 接口 IP:
输出:
3: tunl0@NONE: <NOARP,UP,LOWER_UP> mtu 1480 qdisc noqueue state UNKNOWN group default qlen 1000
link/ipip 0.0.0.0 brd 0.0.0.0
inet 10.244.241.64/32 scope global tunl0
valid_lft forever preferred_lft forever
MASQUERADE 的行为是"用出接口的 IP 做源地址"。master01 访问 Service 时,DNAT 选中了 worker01 上的 Pod(10.244.5.12),包需要通过 tunl0 IPIP 隧道发到 worker01,所以 MASQUERADE 用 tunl0 的 IP(10.244.241.64)作为源 IP,而不是物理网卡的 IP。
这个发现说明:SNAT 改写的源 IP 不一定是物理 IP,而是包实际离开节点时使用的接口 IP。在 IPIP 隧道模式下,这个接口是 tunl0。
对比 hairpin 场景
Hairpin 场景中 REPLY 的 dst 是 192.168.114.148(worker01 物理 IP),因为 hairpin 包不出节点,走本地 cali 接口,MASQUERADE 用的是物理接口 IP。
节点访问跨节点 Pod 时 REPLY 的 dst 是 10.244.241.64(tunl0 IP),因为包走 IPIP 隧道出节点,MASQUERADE 用的是 tunl0 接口 IP。
接口不同,SNAT 地址不同。
五、场景三:NodePort 外部流量¶
5.1 NodePort 基本原理¶
NodePort 是 Service 的一种类型,在每个节点上开放一个端口(默认 30000-32767),外部客户端可以通过 节点IP:NodePort 访问 Service。
当前测试环境的 NodePort Service:
nginx NodePort 10.105.172.12 <none> 80:31330/TCP 34d
nginx-np NodePort 10.104.20.136 <none> 80:31667/TCP 8m
5.2 externalTrafficPolicy: Cluster(默认)¶
默认情况下,NodePort Service 的 externalTrafficPolicy 是 Cluster。这意味着:
- 外部客户端访问
worker01:30080 - iptables DNAT 选中任意后端 Pod(可能在任何节点上)
- 如果选中的 Pod 在其他节点,包需要跨节点转发
跨节点转发时,如果不做 SNAT:
- 包
src=客户端IP, dst=后端PodIP(DNAT 后)发到 worker03 - worker03 上的 Pod 收到包,回包
src=PodIP, dst=客户端IP - 客户端不在集群网络里,回包路由可能出问题——worker03 的路由表不知道客户端 IP 怎么走,可能走默认路由到另一个网关
这就是为什么 NodePort Cluster 模式要做 SNAT:把源 IP 改成接收流量的节点 IP,这样回包一定回到这个节点,conntrack 能正确反向 NAT。
sequenceDiagram
participant C as 外部客户端<br/>1.2.3.4
participant N1 as worker01<br/>192.168.114.148
participant N2 as worker03<br/>192.168.114.150
participant P as nginx Pod<br/>10.244.19.69
C->>N1: SYN dst=192.168.114.148:30080
Note over N1: KUBE-NODEPORTS 匹配<br/>DNAT: dst → 10.244.19.69:80
Note over N1: KUBE-MARK-MASQ 打标记<br/>SNAT: src → 192.168.114.148
N1->>N2: IPIP 封装<br/>src=192.168.114.148 dst=10.244.19.69
N2->>P: 包到达 Pod
P->>N2: 回包 src=10.244.19.69 dst=192.168.114.148
N2->>N1: IPIP 解封装
Note over N1: conntrack 反向 NAT<br/>src → 192.168.114.148:30080
N1->>C: 回包原路返回客户端 5.3 externalTrafficPolicy: Local¶
如果后端 Pod 需要看到真实客户端 IP(比如日志统计、访问控制),可以设 externalTrafficPolicy: Local。
Local 模式下:
- 外部客户端访问
worker01:30080 - iptables 只在本节点的后端 Pod 之间做 DNAT
- 如果本节点没有后端 Pod,直接 DROP(不是转发到其他节点)
- 外部流量不做 SNAT,后端 Pod 看到的源 IP 是真实客户端 IP
- 但节点本机进程访问 NodePort 仍然做 SNAT(用
--src-type LOCAL精确匹配,详见 5.5 节)
5.4 Cluster vs Local 对比¶
| 维度 | Cluster(默认) | Local |
|---|---|---|
| 后端 Pod 选择范围 | 所有节点 | 仅本节点 |
| 本节点无 Pod 时 | 转发到其他节点 | DROP(连接失败) |
| SNAT | 做(源 IP → 节点 IP) | 不做 |
| 后端 Pod 看到的源 IP | 节点 IP | 真实客户端 IP |
| 负载均衡 | 均匀(跨所有 Pod) | 不均匀(取决于 Pod 分布) |
| 适用场景 | 通用 | 需要保留客户端 IP(如 Ingress) |
5.5 iptables 规则差异¶
KUBE-NODEPORTS 链不直接做 DNAT 或打标记,而是跳到 KUBE-EXT 链(externalTrafficChain)。masquerade 逻辑在 KUBE-EXT 链里,根据 externalTrafficPolicy 不同而不同。
Cluster 模式(externalTrafficPolicy: Cluster):
# 在 Worker Node 上执行
sudo iptables -t nat -S KUBE-NODEPORTS | grep nginx-np
sudo iptables -t nat -S KUBE-EXT-O5BZVH4OHNQ6VESY
KUBE-NODEPORTS:
KUBE-EXT(Cluster 模式):
-A KUBE-EXT-O5BZVH4OHNQ6VESY -j KUBE-MARK-MASQ # 所有外部流量打标记
-A KUBE-EXT-O5BZVH4OHNQ6VESY -j KUBE-SVC-O5BZVH4OHNQ6VESY # 跳到 SVC 链做 DNAT
第一条规则无条件打 MASQ 标记——所有从 NodePort 进来的外部流量都做 SNAT。这就是 Cluster 模式下后端 Pod 看到节点 IP 而非客户端 IP 的原因。
Local 模式(externalTrafficPolicy: Local):
将 nginx-np 切换为 Local 模式后:
kubectl patch svc nginx-np -p '{"spec":{"externalTrafficPolicy":"Local"}}'
sudo iptables -t nat -S KUBE-EXT-O5BZVH4OHNQ6VESY
KUBE-EXT(Local 模式):
-A KUBE-EXT-O5BZVH4OHNQ6VESY -s 10.244.0.0/16 -j KUBE-SVC-O5BZVH4OHNQ6VESY # 规则1: Pod 流量,跳 SVC,不打标记
-A KUBE-EXT-O5BZVH4OHNQ6VESY -m addrtype --src-type LOCAL -j KUBE-MARK-MASQ # 规则2: 节点本机流量,打标记
-A KUBE-EXT-O5BZVH4OHNQ6VESY -m addrtype --src-type LOCAL -j KUBE-SVC-O5BZVH4OHNQ6VESY # 规则3: 节点本机流量,跳 SVC
-A KUBE-EXT-O5BZVH4OHNQ6VESY -j KUBE-SVL-O5BZVH4OHNQ6VESY # 规则4: 其他流量(外部),跳 SVL
四条规则,精确区分三种来源:
| 规则 | 匹配条件 | 命中场景 | 打标记? |
|---|---|---|---|
| 1 | -s 10.244.0.0/16 | 本节点 Pod 访问 NodePort | 否 |
| 2 | --src-type LOCAL(非 Pod 网段) | 节点本机进程访问 NodePort | 是 |
| 3 | --src-type LOCAL(非 Pod 网段) | 同上,打标记后跳 SVC | — |
| 4 | 默认(不匹配以上) | 外部客户端访问 NodePort | 否 |
规则 2 和 3 看起来重复——都匹配 --src-type LOCAL。但实际上 iptables 是顺序执行的:规则 1 先匹配 Pod 网段流量(跳走),剩下的非 Pod 流量里,规则 2 给节点本机流量打标记,规则 3 让它跳到 KUBE-SVC 做 DNAT。规则 4 是兜底,处理既不在 Pod 网段也不是本机地址的外部流量。
外部流量命中规则 4,不匹配任何 MASQ 规则,所以不做 SNAT——客户端 IP 被保留。
--src-type LOCAL 是 addrtype 模块的匹配条件,判断源 IP 是否是本机地址(绑定在某个本地接口上的 IP)。外部客户端的 IP 不会绑定在节点任何接口上,所以不匹配。
六、conntrack 视角:三种 SNAT 的对比¶
6.1 对比表¶
| 场景 | ORIGINAL 方向 | REPLY 方向 | SNAT 证据 |
|---|---|---|---|
| Hairpin | src=PodIP, dst=ClusterIP | src=PodIP:80, dst=物理IP | REPLY 的 dst 是物理 IP(包走本地 cali 接口) |
| 节点访问 Service | src=物理IP, dst=ClusterIP | src=PodIP, dst=tunl0 IP | REPLY 的 dst 是 tunl0 IP(包走 IPIP 隧道) |
| NodePort Cluster | src=客户端IP, dst=节点IP:NodePort | src=PodIP, dst=接收节点IP | REPLY 的 dst 是接收节点的物理 IP,不是客户端 IP |
| NodePort Local | src=客户端IP, dst=节点IP:NodePort | src=PodIP, dst=客户端IP | REPLY 的 dst 是客户端真实 IP,没有 SNAT |
6.2 怎么从 conntrack 条目判断是否发生了 SNAT¶
conntrack 条目分 ORIGINAL(NAT 之前)与 REPLY(NAT 之后)两行,判断 SNAT 只需看 REPLY 方向的 dst 字段:出现节点 IP(或 tunl0 IP)就是做了 SNAT,是客户端真实 IP 就是没做 SNAT。完整的判读规则、字段含义与更多示例见 conntrack 命令详解。
Hairpin 场景的 conntrack 条目(来自 curl 全链路实测):
tcp 6 114 TIME_WAIT src=10.244.5.12 dst=10.100.241.29 sport=34188 dport=80
[ASSURED] src=10.244.5.12 dst=192.168.114.148 sport=80 dport=34188 use=1
- ORIGINAL:
src=10.244.5.12, dst=10.100.241.29(Pod → ClusterIP) - REPLY:
src=10.244.5.12:80, dst=192.168.114.148(Pod:80 → 节点IP)
REPLY 方向的 dst=192.168.114.148(节点 IP)就是 SNAT 的铁证。
6.3 NodePort 两种模式的 conntrack 实测对比¶
从 master01(192.168.114.145)访问 worker01(192.168.114.148)上的 nginx-np NodePort:
Cluster 模式(做了 SNAT):
tcp 6 111 TIME_WAIT src=192.168.114.145 dst=192.168.114.148 sport=49076 dport=31667
[ASSURED] src=10.244.5.15 dst=192.168.114.148 sport=80 dport=24420 use=1
- ORIGINAL: master01 → worker01:31667
- REPLY: nginx Pod → worker01(SNAT 把源 IP 改成了 worker01 的 IP)
Local 模式(没有 SNAT):
tcp 6 111 TIME_WAIT src=192.168.114.145 dst=192.168.114.148 sport=38414 dport=31667
[ASSURED] src=10.244.5.15 dst=192.168.114.145 sport=80 dport=38414 use=1
- ORIGINAL: master01 → worker01:31667
- REPLY: nginx Pod → master01(客户端 IP 保留,没有 SNAT)
唯一区别是 REPLY 方向的 dst:Cluster 模式是 192.168.114.148(接收节点),Local 模式是 192.168.114.145(客户端真实 IP)。
6.4 nginx access.log 交叉验证¶
Local 模式下 nginx Pod 的 access.log:
源 IP 是 192.168.114.145(master01),和 conntrack 的 REPLY dst 一致。conntrack 和应用层日志互相印证。
七、三种场景的 Mermaid 总图¶
场景 1:Hairpin NAT¶
flowchart TD
H1["Pod 10.244.5.12<br/>访问 Service"] --> H2{"DNAT 选中<br/>自己?"}
H2 -->|"是"| H3["KUBE-MARK-MASQ<br/>打标记"]
H3 --> H4["MASQUERADE<br/>src → 节点 IP"]
H4 --> H5["包到达自己<br/>回包原路返回"]
classDef problem fill:#FCE7F3,stroke:#DB2777,color:#0F172A
classDef snat fill:#FEF3C7,stroke:#D97706,color:#0F172A
classDef normal fill:#DBEAFE,stroke:#2563EB,color:#0F172A
class H3,H4 snat
class H1,H2 normal 场景 2:节点访问 Service¶
flowchart TD
N1["节点进程<br/>curl ClusterIP"] --> N2["OUTPUT 链<br/>KUBE-SERVICES"]
N2 --> N3["KUBE-SVC: ! -s Pod网段<br/>打 KUBE-MARK-MASQ 标记"]
N3 --> N4["DNAT 选后端 Pod"]
N4 --> N5{"后端在本节点?"}
N5 -->|"是"| N6["走 cali 接口<br/>MASQUERADE src → 物理 IP"]
N5 -->|"否"| N7["走 IPIP 隧道<br/>MASQUERADE src → tunl0 IP"]
classDef snat fill:#FEF3C7,stroke:#D97706,color:#0F172A
classDef normal fill:#DBEAFE,stroke:#2563EB,color:#0F172A
class N3,N6,N7 snat
class N1,N2,N4,N5 normal 场景 3:NodePort 外部流量¶
flowchart TD
E1["外部客户端<br/>访问 NodePort"] --> E2{"externalTrafficPolicy"}
E2 -->|"Cluster"| E3["KUBE-EXT 无条件打标记<br/>MASQUERADE src → 节点 IP"]
E2 -->|"Local"| E4["KUBE-EXT --src-type LOCAL<br/>外部流量不匹配<br/>不打标记"]
E3 --> E5{"后端在本节点?"}
E5 -->|"否"| E6["跨节点 IPIP<br/>回包原路返回"]
E5 -->|"是"| E7["直连 Pod"]
E4 --> E8{"本节点有 Pod?"}
E8 -->|"否"| E9["DROP"]
E8 -->|"是"| E7
classDef snat fill:#FEF3C7,stroke:#D97706,color:#0F172A
classDef normal fill:#DBEAFE,stroke:#2563EB,color:#0F172A
classDef drop fill:#FEE2E2,stroke:#DC2626,color:#0F172A
class E3 snat
class E9 drop
class E1,E2,E4,E5,E8 normal 八、验证命令清单¶
8.1 查看 SNAT 相关的 iptables 规则¶
# KUBE-MARK-MASQ 链
sudo iptables -t nat -L KUBE-MARK-MASQ -n
# KUBE-POSTROUTING 链(SNAT 实际执行点)
sudo iptables -t nat -L KUBE-POSTROUTING -n
# KUBE-SERVICES 链开头几条(具体 Service 匹配,跳到 KUBE-SVC)
sudo iptables -t nat -S KUBE-SERVICES | head -5
# KUBE-SVC 链(masquerade 非 Pod 网段流量 + DNAT 负载均衡)
sudo iptables -t nat -S KUBE-SVC-XXXXX | head -5
# KUBE-NODEPORTS 链(NodePort 入口)
sudo iptables -t nat -S KUBE-NODEPORTS
# 某个 Service 的 KUBE-SEP 链(hairpin 打标记规则)
sudo iptables -t nat -S | grep 'KUBE-SEP' | grep 'KUBE-MARK-MASQ'
8.2 查看 conntrack 证据¶
# Hairpin 场景:Pod 内跑 curl,同时查 conntrack
# REPLY 方向出现节点 IP = 发生了 SNAT
kubectl exec -it $POD_NAME -- curl -s -o /dev/null nginx-test &
sudo conntrack -L | grep 'dport=80' | head -5
# 节点访问 Service 场景
curl -s -o /dev/null http://10.100.241.29 &
sudo conntrack -L | grep '10.100.241' | head -5
# NodePort 场景
curl -s -o /dev/null http://192.168.114.148:30080 &
sudo conntrack -L | grep '30080' | head -5
conntrack -L 的完整选项、过滤参数与条目字段解析见 conntrack 命令详解。
8.3 验证 externalTrafficPolicy 差异¶
# Cluster 模式:后端 Pod 看到的源 IP
kubectl exec -it <nginx-pod> -- cat /var/log/nginx/access.log | tail -1
# Local 模式:后端 Pod 看到的源 IP
# 切换 externalTrafficPolicy 后重新访问,对比 access.log 里的源 IP
九、常见问题排查¶
9.1 后端 Pod 看到的源 IP 全是节点 IP¶
原因:externalTrafficPolicy 是 Cluster(默认),所有 NodePort 流量都做了 SNAT。
解决:如果需要保留客户端 IP,改 externalTrafficPolicy: Local。但注意 Local 模式下,如果本节点没有后端 Pod,连接会直接失败。
9.2 NodePort 访问超时¶
排查顺序:
kubectl get endpoints <svc>—— Endpoint 是否为空sudo iptables -t nat -L KUBE-NODEPORTS -n—— NodePort 规则是否存在- 如果是 Local 模式:本节点有没有后端 Pod?
kubectl get pods -o wide | grep <node>
9.3 conntrack 表满¶
每个访问 Service 的连接至少 1 条 conntrack 条目。如果 NodePort 外部流量大 + 短连接多,conntrack 表可能满。
# 查看 conntrack 表使用量
sudo cat /proc/sys/net/netfilter/nf_conntrack_count
sudo cat /proc/sys/net/netfilter/nf_conntrack_max
# 如果接近 max,需要调大限制
echo 262144 | sudo tee /proc/sys/net/netfilter/nf_conntrack_max
十、相关阅读¶
- [Kube-Proxy 工作原理](./kube-proxy.md)——iptables 三级链、DNAT 机制、iptables vs IPVS
- [一行 curl 的完整旅程](./curl-full-link.md)——hairpin NAT 的完整实测数据(strace + conntrack)
- [Calico 跨节点通信](./calico-bgp.md)——SNAT 后的跨节点包怎么走 IPIP 隧道
- [CoreDNS:Service 域名解析](./service-dns-to-clusterip.md)——DNS 查询也经过 KUBE-SERVICES → KUBE-SEP DNAT