Pod 能连内网却出不了外网?不是 CNI——是 SNAT 没开门
场景:Pod 内 curl 外网 API 超时,但 curl 集群内 Service 秒回——开发改了三次应用配置没效果 路径:坐标(Pod 内外网对比)→ 分层(Pod → CRI → Node iptables → 集群 kube-proxy)→ 路径(4 步排查链)→ 定位(查错层:应用日志 vs 网络栈)→ 标点(修复 + Check-list) K8s 版本:v1.28,Flannel CNI
上篇讲了两个 Service 端口冲突导致诡异超时的问题——调度器给两个不同 Pod 分配了相同节点端口,客户端请求被错误路由到错误的 Pod。这次我们来看另一个让 curl 超时的场景:Pod 内能访问 Service,但一出集群就超时。
新部署上线后,业务方反馈"应用一直在加载中,页面转圈 30 秒后报错"。查看应用日志——"Connection to httpbin.org timed out"。开发第一反应是 API 地址配错了——检查配置:地址没错。kubectl exec 进 Pod 测了一下——curl httpbin.org 卡死直到超时,但 curl service-a:8080 秒回 200。不是 API 地址配错了——是 Pod 的包出了集群就没人在意了。Node 上的 iptables 没给它做 SNAT(源地址转换,将 Pod 的私网 IP 替换为 Node 的公网 IP),包带着 Pod 的私网 IP 出门,外网路由器不知道怎么回就把它扔了。

【坐标】现象:内网通、外网不通——最迷惑人的网络故障
K8s 网络排障中,有一类问题特别容易把人带偏:集群内部正常,一到边界就断。现象单一但根因藏在边界层,不是应用层。
Layer 1 — 部署背景
| 部署项 | 配置 |
|---|---|
| 消费方 | Pod A,worker-app,10.244.1.15 |
| 目标 API | httpbin.org:443 |
| 集群内通信 | curl service-a:8080 → 秒回 200 |
| 网络插件 | Flannel(vxlan 模式),Pod CIDR 10.244.0.0/16 |
| 集群出站 | 节点 eth0,公网 IP,有默认路由到互联网网关 |
Pod A 运行正常(Running),集群内通信无问题。但凡是发往 Pod CIDR 和 Service CIDR 之外的流量,全部超时。
Layer 2 — 两个 curl 的矛盾结果
进入 Pod 测了两次——不到 1 分钟就拿到了最关键的证据。
测集群内 Service:秒回 200。测外网 httpbin.org:卡死直到超时。同一个 Pod、同一个 network namespace、同一个时刻——一个通一个不通。
这说明应用没问题、Pod 的协议栈没问题。问题不在"能不能发出去"——SYN 包正常到了外网服务器——而在"发出去之后怎么回来"。包出了 Node 之后,回不来了。
Layer 3 — 方向判断
从这个矛盾现象可以锁定排查范围:
内网通 + 外网不通
→ 不可能是 Pod 协议栈问题(能正常发收)
→ 不可能是 Service 层问题(ClusterIP 和 kube-proxy 正常)
→ 不可能是 CNI 连通性问题(跨 Node Pod 也能通)
→ 锁定方向:集群出站流量处理——NAT / 路由 / 网关
问题不在"能不能发出去",在"发出去之后怎么回来"。 Pod 的私网 IP 在外部网络是不可路由的,必须由 Node 做 SNAT 换成公网 IP。如果这一步没做,包有去无回。

【分层】逐层排查:从 Pod 出发,一路追到集群边界
排查顺序表
| 层 | 排查对象 | 关键命令 | 预期正常 | 异常信号 |
|---|---|---|---|---|
| Layer 1 | Pod 网络 | kubectl exec curl 内外网对比 |
内网通外网通 | 内网通外网不通 → 边界问题 |
| Layer 2 | CRI 容器网络 | crictl inspect <container> |
网络命名空间正常 | netns 异常 → containerd 问题 |
| Layer 3 | Node iptables NAT | iptables -t nat -L POSTROUTING |
MASQUERADE 规则存在 | 规则缺失 → SNAT 未生效 |
| Layer 4 | kube-proxy 配置 | kubectl get cm kube-proxy -n kube-system |
masqueradeAll: true |
false → 需启用 |
Layer 1: Pod 网络 — 先确认问题边界
直接在 Pod 内执行内外网对比测试。SYN 发出去了但完全没有 SYN-ACK 回来——重试了 3 次后超时。包确实发出去了,但回不来。

Layer 2: CRI 层 — 快速排除容器运行时
排查到这一步,有个分支判断:Pod 内能 curl 说明网络命名空间正常,CRI(Container Runtime Interface,K8s 通过 CRI 与 containerd 交互管理容器生命周期)层面一般不会有问题。但为了不跳层,用 crictl 确认一下容器网络状态。
$ crictl ps --name worker-app
CONTAINER IMAGE CREATED STATE NAME
a1b2c3d4 worker:1.0 10min ago Running worker-app
$ crictl inspect a1b2c3d4 | jq '.info.runtimeSpec.linux.namespaces'
# netns 正常挂载,无异常
容器运行时正常。排除 containerd 层面的干扰,接下来上 Node——90% 的概率问题在这里。
Layer 3: Node 层 — 真相所在
SSH 到 Pod 所在的 Node,检查 nat 表的 POSTROUTING 链——这是 K8s 做源地址转换的地方。kubelet(K8s 节点代理,负责管理 Pod 生命周期并与 API Server 通信)本身不做 NAT,NAT 规则由 kube-proxy(K8s 网络代理组件,运行在每个 Node 上,维护 iptables/IPVS 规则)生成。
正常的集群应该有对应 Pod CIDR 的 MASQUERADE 规则(MASQUERADE all -- 10.244.0.0/16 !10.244.0.0/16),但当前 Node 上只有 RETURN 规则,缺少 MASQUERADE 行。
kube-proxy 没生成 MASQUERADE 规则——包从 Pod 发出后经过 Node,源 IP 依然是 10.244.1.15。外网的 HTTPBin 服务器看到源 IP 是 10.244.x.x——这是私有地址段,互联网路由器不会转发回包,直接丢弃。

在 Node 上抓包确认——包确实出去了,但源 IP 没变:
$ tcpdump -i any -n host 18.154.144.104
11:38:15.123456 IP 10.244.1.15.43124 > 18.154.144.104.443: Flags [S]
11:38:16.123789 IP 10.244.1.15.43124 > 18.154.144.104.443: Flags [S]
如果 SNAT 正常工作,源 IP 应该是 Node 的公网 IP(如 192.168.1.10),而不是 Pod 的 10.244.1.15。正因为缺少 MASQUERADE 规则,kube-proxy 没有对出站流量做源地址转换,包带着私网 IP 出了门,就再也回不来了。
Layer 4: 集群层 — 检查 kube-proxy 配置
为什么 kube-proxy 没生成 MASQUERADE 规则?查它的配置:
$ kubectl -n kube-system get cm kube-proxy -o yaml
核心字段:
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
clusterCIDR: 10.244.0.0/16
masqueradeAll: false # ← 默认为 false!
masqueradeAll: false 意味着 kube-proxy 只对发往不同 Node 的 Pod IP 流量做 SNAT,对发往外部网络的流量不做 SNAT。这是 K8s 的默认行为——ClusterIP 的流量通过 kube-proxy 的 iptables 规则自动处理 SNAT,但 Pod 直接发往外部的流量不在 kube-proxy 默认管理的范围内,需要显式开启 masqueradeAll。

【路径】🔍 下次先跑这个命令
4 步排查链
Pod 内网通外网不通时,按这个顺序查:
# 全量排查命令,拷贝即用
# Step 1: 内外网对比 —— 确认问题边界
kubectl exec <pod> -- curl -v --connect-timeout 5 <service-in-cluster>:port
kubectl exec <pod> -- curl -v --connect-timeout 10 https://httpbin.org/get
# → 内网通 + 外网不通 = 边界问题,不要查应用层
# Step 2: Node 上查 iptables NAT 规则 —— 核心
kubectl get pods -o wide | grep <pod> # 找 Pod 所在 Node
ssh <node>
iptables -t nat -L KUBE-POSTROUTING -v
# → 应有 MASQUERADE all -- 10.244.0.0/16 !10.244.0.0/16
# → 缺少 = SNAT 没生效
# Step 3: Node 上 tcpdump 抓包 —— 看源 IP
ssh <node>
tcpdump -i any -n host <external-ip>
# → 源 IP 是 Pod IP(私网)= 没 SNAT
# → 源 IP 是 Node IP(公网)= 网关/路由问题
# Step 4: 查 kube-proxy 配置 —— 根因
kubectl -n kube-system get cm kube-proxy -o yaml | grep masqueradeAll
# → false = 默认值,需改为 true

异常判断快速表
| 命令 | 正常输出 | 异常信号 | 下一步 |
|---|---|---|---|
kubectl exec curl <svc> |
HTTP 200 | timeout / refused | 检查 Pod 网络 |
kubectl exec curl <外网> |
HTTP 200 | timeout → 边界问题 | 查 Node iptables |
iptables -L KUBE-POSTROUTING -v |
MASQUERADE 行 | 无 MASQUERADE → SNAT 缺 | 查 kube-proxy 配置 |
tcpdump -i any host <外网IP> |
源 IP = Node IP | 源 IP = Pod IP → 没做 SNAT | 启用 masqueradeAll |
kubectl get cm kube-proxy -o yaml |
masqueradeAll: true |
false → 需要改 |
编辑配置重启组件 |
【定位】查错层比不查更浪费时间
❌ 错误排查方向
"外网不通 → 网络有问题 → 查 CNI"
典型的内心 OS:
curl 外网超时
→ 是不是 DNS 解析不对?→ nslookup → 能解析出 IP
→ 是不是 CNI 插件坏了?→ kubectl get po -n kube-system
→ Flannel Pod 都在 Running → 重启 flannel Pod → 无效
→ 是不是 Node 网络有问题?→ ping 外网 → Node 能通!
→ 那肯定是应用配置问题 → 改应用代理设置 → 重启 Pod → 无效
→ 放弃,联系集群管理员
这个过程消耗 30-60 分钟。你以为在排查网络,其实跳过了最关键的 iptables 层。 从 CNI 直接跳到 Node 网络——但 K8s 的出站流量管理不在 CNI 里(CNI 管 Pod 互通的隧道和路由),也不在 Node 原生网络里(Node 本身该通还是通),而在 kube-proxy 生成的 iptables NAT 规则里。
CNI 解决的是"Pod 之间怎么通",kube-proxy 解决的是"Pod 的流量怎么出集群"。两者不是同层。
✅ 正确排查方向
curl 外网超时
→ Step 1: kubectl exec curl 外网 + 内网 → 确认边界问题
→ Step 2: 找到 Pod 所在 Node → ssh → iptables -t nat -L KUBE-POSTROUTING
→ Step 3: 发现缺 MASQUERADE → 查 kube-proxy config
→ Step 4: masqueradeAll: false → 改为 true → 重启 kube-proxy
→ Step 5: 再测 curl → 200 ✅
总耗时:5-10 分钟。
两者的本质差异:K8s 的出站流量链路分了三段——CNI 管 Pod 互通、kube-proxy 管 NAT 出站、Node 网络管公网路由。你看到的"外网不通"可能停留在 CNI 层和 Node 网络层,但真正的锅在中间那层——iptables 的 MASQUERADE 规则。CNI 告诉你 Pod 间通了、Node 网络告诉你 Node 自己能上网——你就以为都正常了。你跳过了中间那一层。查对层比查得快更重要——查错层,起步就是 30 分钟白费。

【标点】修复 + Check-list
修复:启用 masqueradeAll
方案 A:编辑 kube-proxy ConfigMap(推荐)
$ kubectl -n kube-system edit cm kube-proxy
将 masqueradeAll: false 改为 true:
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
masqueradeAll: true # ← 改这里
重启 kube-proxy 加载新配置:
$ kubectl -n kube-system rollout restart ds kube-proxy
daemonset.apps/kube-proxy restarted
方案 B:手动补 iptables 规则(快速验证)
不想改配置时,先手动加一条验证是不是这个问题:
$ iptables -t nat -A KUBE-POSTROUTING -s 10.244.0.0/16 ! -d 10.244.0.0/16 -j MASQUERADE
然后再测——通了说明根因确认,再走方案 A 持久化。
修复后验证:
$ kubectl exec worker-app -- curl -s -o /dev/null -w "%{http_code}" \
--connect-timeout 10 https://httpbin.org/get
200

Pod 外网不通排查 Check-list
□ kubectl exec <pod> -- curl -v --connect-timeout 5 <cluster-svc>:<port>
→ 集群内通信正常(不正常 → 查 Pod 状态/Service 配置)
□ kubectl exec <pod> -- curl -v --connect-timeout 10 https://httpbin.org/get
→ 不通 → 边界问题,不查应用日志
□ kubectl get pod -o wide | grep <pod>
→ 记录 Pod 所在的 Node
□ ssh <node> && iptables -t nat -L KUBE-POSTROUTING -v
→ 是否有 MASQUERADE 规则
→ 源 10.244.0.0/16,目标非 10.244.0.0/16 → MASQUERADE
□ ssh <node> && tcpdump -i any -n host <external-ip>
→ 源 IP 是 Pod IP → 没 SNAT
→ 源 IP 是 Node IP → SNAT 正常,查网关/路由
□ kubectl -n kube-system get cm kube-proxy -o yaml | grep masqueradeAll
→ true → CNI/路由配置问题
→ false / 未设 → 启用 masqueradeAll: true
□ 修复后重新验证
→ curl httpbin.org → 200 ✅
□ 如果 masqueradeAll=true 仍然不通
→ 检查 CNI 是否配置了出站 NAT(Flannel 的 `net-conf.json` 中 `DirectRouting`)
→ 检查节点默认网关路由是否正确
→ 检查集群网络策略(NetworkPolicy egress 规则)
K8s 问题排查的终点不是修好了——是把排查路径写成 check-list,下次别人遇到同样问题不用重头再查一遍。
附:完整命令清单
# === 1. Pod 内外网对比 ===
kubectl exec <pod> -- curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 <svc>:<port>
kubectl exec <pod> -- curl -v --connect-timeout 10 https://httpbin.org/get
kubectl exec <pod> -- tcpdump -i eth0 -n host httpbin.org
# === 2. Node iptables NAT 检查 ===
kubectl get pod -o wide | grep <pod>
ssh <node>
iptables -t nat -L KUBE-POSTROUTING -v
iptables -t nat -L POSTROUTING -v
tcpdump -i any -n host <external-ip>
# === 3. kube-proxy 配置检查 ===
kubectl -n kube-system get cm kube-proxy -o yaml
kubectl -n kube-system get po -l k8s-app=kube-proxy
kubectl -n kube-system logs <kube-proxy-pod>
# === 4. 修复:启用 masqueradeAll ===
kubectl -n kube-system edit cm kube-proxy
# 将 masqueradeAll: false 改为 true
kubectl -n kube-system rollout restart ds kube-proxy
# === 5. 快速验证(手动加规则) ===
iptables -t nat -A KUBE-POSTROUTING -s 10.244.0.0/16 ! -d 10.244.0.0/16 -j MASQUERADE
# === 6. 验证修复 ===
kubectl exec <pod> -- curl -s -o /dev/null -w "%{http_code}" --connect-timeout 10 https://httpbin.org/get
# 返回 200 ✅
# === 7. 附加排查(masqueradeAll=true 仍不通时) ===
kubectl -n kube-system get cm kube-proxy -o yaml | grep -A5 "clusterCIDR\|masqueradeAll"
kubectl describe node <node> | grep -A5 "InternalIP\|ExternalIP"
ip route show default
# CNI 出站 NAT 配置
kubectl -n kube-system get cm kube-flannel-cfg -o yaml 2>/dev/null | grep -i "net-conf"
记住一句话:排查 K8s 问题不是在查 Pod——是在查 Pod 所在的整条链路。Pod 到终端的每一跳、每一层策略、每一条 iptables 规则,都是嫌疑对象。
上篇我们聊 Service 端口冲突,这篇聊了 Pod 出站 SNAT 缺失。下篇我们聊 Istio sidecar 注入后应用启动变慢——sidecar 容器是怎么静悄悄地拖慢 Pod 启动速度的。
🔗 个人博客:https://opencao.cn 📺 公众号:Ai拆代码的曹操 🌟 知识星球:Ai拆代码的曹操