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 出门,外网路由器不知道怎么回就把它扔了。

内外网对比:curl 集群内 Service 秒回 200,curl 外网卡死超时


【坐标】现象:内网通、外网不通——最迷惑人的网络故障

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 到外网的包路径:Pod → Node → SNAT → 网关 → 外网——标注 SNAT 断点


【分层】逐层排查:从 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 次后超时。包确实发出去了,但回不来。

Pod 内 tcpdump:只看到 SYN 没有 SYN-ACK——说明包出门后丢了

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——这是私有地址段,互联网路由器不会转发回包,直接丢弃。

iptables -t nat -L KUBE-POSTROUTING:缺少 MASQUERADE 规则

在 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

kube-proxy 配置确认:masqueradeAll: false


【路径】🔍 下次先跑这个命令

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

4 步排查命令 + 异常判断标准

异常判断快速表

命令 正常输出 异常信号 下一步
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 分钟白费。

错误排查(在 CNI 和应用层打转)vs 正确排查(逐层查 iptables NAT)


【标点】修复 + 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

kube-proxy 配置修复前后对比 + 恢复验证

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拆代码的曹操