
一个 Pod 能访问另一个 Pod，却访问不了它的 Service，该先看 Calico 还是 kube-proxy？如果同节点能通、跨节点不通，排查方向又有什么不同？

这两个问题需要沿数据包实际走过的路径区分。Calico 为 Pod 建立网络接口、路由和策略；kube-proxy 把 Service 地址转换为后端地址。访问 ClusterIP 时，数据包在源节点完成后端选择，随后仍要依靠 Calico 配置的网络抵达那个 Pod。

本文使用 Calico 作为主 CNI，禁用 kindnet，保留 kube-proxy。具体组合是 Kubernetes v1.35.1、Calico v3.33.0、kube-proxy iptables 模式、Calico iptables 数据面，以及 `VXLAN Always`。BGP、IP-in-IP 和 Calico eBPF 均未启用。这些条件决定了下面看到的规则和路由，不能把图直接套到 Calico eBPF 替代 kube-proxy 的集群。

实验通过 SSH 在 `zj` 上运行三节点 kind：一个控制面、两个 worker。kind 节点本身是 Docker 容器，节点间互联通过宿主机上的 Docker 网络；图中的节点 `eth0` 因此是节点容器的网卡。实验验证的是节点网络命名空间中的转发机制，不用这套拓扑推断物理交换网络的性能。

配置、操作说明和原始证据见 [k8sdev/pod-network-calico](https://github.com/jimyag/k8sdev/tree/095c6e71638827967680c537b36b0fc6dad7599c/pod-network-calico)。下面保留与机制解释直接相关的结果。

## 从访问目标辨认参与的组件

| 访问方式 | kube-proxy 在这条路径中的工作 | Calico 在这条路径中的工作 |
| --- | --- | --- |
| 同节点 Pod IP | 不需要 Service DNAT | veth、主机路由和端点策略 |
| 跨节点 Pod IP | 不需要 Service DNAT | 路由到远端节点，经 VXLAN 传送并交给目标 Pod |
| ClusterIP | 选后端，改写目标地址和端口 | 转发到 DNAT 后的 Pod IP，并执行策略 |
| Pod 访问集群外地址 | 目标不是 Service 时，不承担 Service 转发 | 按节点路由出站，按 IP 池条件决定是否 SNAT |
| 集群外访问 NodePort | 在入口节点选后端，按流量策略决定源地址处理 | 将转换后的流量送到后端 Pod |

“不需要 Service DNAT”不表示数据包绕过了 Linux netfilter。Calico 和 kube-proxy 的规则都可能挂在相同 hook 上，区别在于哪些规则匹配并改变了这个包。

本次用 `client` 和 `local` 两个 Pod 验证同节点路径，`remote` 固定在另一台 worker 上。名为 `remote` 的 Service 只选择跨节点后端，使每次抓包的终点确定。业务进程在 TCP 8080 上返回自己的名字和看到的客户端 IP；ClusterIP 使用 80，NodePort 使用 30080。

本次实测地址如下，后文的路由与抓包均来自这组对象：

| 对象 | 地址 |
| --- | --- |
| worker 节点 / worker2 节点 | `172.31.0.2` / `172.31.0.3` |
| client / local | `10.244.80.65` / `10.244.80.66` |
| remote | `10.244.158.65` |
| remote Service | `10.98.235.247:80` |
| worker / worker2 的 VXLAN 端点 | `10.244.80.64` / `10.244.158.64` |

官方[版本要求](https://docs.tigera.io/calico/3.33/getting-started/kubernetes/requirements)列出了 Calico 3.33 测试覆盖的 Kubernetes 版本。这里固定具体版本，是为了让源码与运行行为可核对。节点中的 `iptables` 即使使用 nf_tables 后端，也不能据此说 kube-proxy 已切换为原生 nftables 模式；判断代理模式要看 kube-proxy 配置与生成的规则结构。

## Pod 发包前，Calico 已经建立了什么

CNI 插件参与 Pod 网络创建时，并不处理之后的每一个请求。调用链从 kubelet 请求创建 sandbox 开始，容器运行时在网络配置过程中调用 Calico CNI。Calico 分配地址，进入 Pod 的网络命名空间配置接口和路由，再把端点信息交给数据存储。

[Calico 的 `CmdAddK8s`](https://github.com/projectcalico/calico/blob/v3.33.0/cni-plugin/pkg/k8s/k8s.go)把这几件事连接起来：调用 IPAM 获取地址，通过 `DoNetworking` 建立数据路径，再以 `CreateOrUpdate` 保存端点信息。这里的 WorkloadEndpoint 是 Calico 描述工作负载网络的模型，不要把它直接等同于一个独立存储的 Kubernetes CRD。

在本文的 Kubernetes datastore 模式下，[WorkloadEndpoint 的后端实现](https://github.com/projectcalico/calico/blob/v3.33.0/libcalico-go/lib/backend/k8s/resources/workloadendpoint.go)会修改 Pod 的 Calico annotations，并由 Pod 数据构造端点视图。源码解释了使用 annotations 的两个原因：避免直接修改 PodStatus 与 kubelet 更新竞争，以及尽早把地址信息送到 Felix。

```mermaid
sequenceDiagram
    participant K as kubelet
    participant R as CRI 运行时
    participant C as Calico CNI / IPAM
    participant L as 节点 Linux 内核
    participant A as API Server
    participant F as Felix
    K->>R: RunPodSandbox
    R->>C: CNI ADD：容器标识与 netns
    C->>A: 查询 Pod、分配 IPAM 地址
    C->>L: 创建 veth，配置 Pod 地址与路由
    C->>L: 配置宿主机端路由和 sysctl
    C->>A: 更新 Pod 的 Calico 端点 annotations
    par Felix 异步协调
        A-->>F: watch：Pod、节点、IPAM、策略变化
        F->>L: 协调端点策略、远端路由与隧道状态
    and 运行时继续创建
        C-->>R: 返回 CNI result
        R-->>K: 返回 sandbox 结果
    end
    Note over C,F: 控制面更新与节点协调异步推进；图不表示 CNI 返回前所有远端节点都已收敛
```

CNI 配置完成和全网状态收敛之间仍有区别。失败定位时，需要分别确认本地接口是否建立、Felix 是否收到端点、远端路由是否已编程，而不是只看 Pod 已有 `podIP`。

## /32 地址和 169.254.1.1 网关如何工作

Calico 的普通 IPv4 workload 接口使用 /32 地址。Pod 看不到一个包含其他 Pod 的直连子网，发往其他地址的包交给默认路由。

[`DoWorkloadNetnsSetUp`](https://github.com/projectcalico/calico/blob/v3.33.0/cni-plugin/pkg/dataplane/linux/dataplane_linux.go)先给 `169.254.1.1/32` 添加 link scope 路由，再把需要的 IPv4 路由指向这个下一跳。这样，即使 Pod 地址是 /32，内核仍知道下一跳可以通过 `eth0` 直接到达。

Pod 为 `169.254.1.1` 发 ARP 时，宿主机侧 veth 开启的 `proxy_arp` 负责回应。这个网关地址不需要逐一配置在宿主机 veth 上。`configureSysctls` 还把 `proxy_delay` 设为 0，并开启接口转发；源码注释明确指出，使用 proxy ARP 可以少维护一份地址，也少一个发生地址冲突的地方。

客户端网络命名空间中的实际配置是：

```text
eth0: inet 10.244.80.65/32，MTU 1450
default via 169.254.1.1 dev eth0
169.254.1.1 dev eth0 scope link
```

对应宿主机接口 `cali747ff840379` 的 `proxy_arp=1`、`proxy_delay=0`、`forwarding=1`，而 `ip -4 addr show dev cali747ff840379` 没有输出 IPv4 地址。这与源码中的虚拟下一跳设计相吻合。

这里的两张表各管一段：Pod 的路由表决定把包交给宿主机，宿主机的路由表决定从哪一个接口继续转发。知道虚拟网关地址，并不意味着所有 Pod 都在同一个二层广播域中。

## 同节点 Pod 通过主机路由相遇

客户端包从 Pod `eth0` 进入宿主机侧 `cali...` veth。目标 Pod 也有对应的宿主机 veth 和主机路由，Linux 根据目的 Pod IP 选中那个接口，把包送进目标网络命名空间。

[`SetupRoutes`](https://github.com/projectcalico/calico/blob/v3.33.0/cni-plugin/pkg/dataplane/linux/dataplane_linux.go)负责为普通 workload 地址建立宿主机端路由。这条路径不要求把两个 veth 接到 Linux bridge 上，也不需要经 VXLAN 绕到远端再回来。

源节点实际安装了下面两条主机路由：

```text
10.244.80.65 dev cali747ff840379 scope link metric 1024
10.244.80.66 dev calid5f5981a8e0 scope link metric 1024
```

`client` 请求 `local` 后得到 `{"server":"local","peer":"10.244.80.65"}`，后端确实看到了客户端 Pod 地址。

```mermaid
flowchart LR
    subgraph CP[客户端 Pod]
        C[进程] --> CE[eth0]
    end
    subgraph HOST[同一节点的网络命名空间]
        CV[cali 客户端接口] --> ROUTE[路由与转发策略]
        ROUTE --> SV[cali 服务端接口]
    end
    subgraph SP[服务端 Pod]
        SE[eth0] --> S[监听 8080 的进程]
    end
    CE --> CV
    SV --> SE
```

这是普通 Pod IP 通信：目的地址不变，后端看到的源地址仍是客户端 Pod IP。若这条路径已经不通，应先确认本地 veth、Pod 路由和 Calico 策略，跨节点隧道通常还不是第一检查点。

## 跨节点多出的三层映射

跨节点时，源节点没有目标 Pod 的本地 veth，需要把包交给目标所在节点。本实验使用 VXLAN，涉及三组不同的地址：

- Pod IP：业务请求的源地址与目的地址。
- VTEP 地址：VXLAN 隧道端点地址，用作经 `vxlan.calico` 转发的下一跳。
- 节点地址：外层 UDP 包的源地址和目的地址，底层网络实际负责路由这一层。

路由表把远端 Pod 网段指向远端 VTEP；邻居表关联 VTEP IP 与隧道 MAC；FDB 再关联隧道 MAC 与远端节点地址。FDB 在这里是 VXLAN 设备使用的转发表，不代表 Calico 把所有 Pod 接在一个普通 Linux bridge 上。

对远端后端 `10.244.158.65`，源节点上的对应关系是：

```text
路由：10.244.158.64/26 via 10.244.158.64 dev vxlan.calico onlink mtu 1450
邻居：10.244.158.64 lladdr 66:b0:94:1d:fb:92 PERMANENT
FDB： 66:b0:94:1d:fb:92 dst 172.31.0.3 self permanent
设备：vxlan id 4096 local 172.31.0.2 dev eth0 dstport 4789 nolearning
```

这里是控制面编程的邻居与 FDB，不依赖业务包先经过广播学习。源节点还存在本地 IPAM block 的 `blackhole 10.244.80.64/26`，实际 Pod 则有更精确的主机路由。按最长前缀匹配，已分配 Pod 会命中主机路由，块内没有更精确路由的目标会被 blackhole 截住，而不是沿默认路由发到节点外。

[`L3RouteResolver`](https://github.com/projectcalico/calico/blob/v3.33.0/felix/calc/l3_route_resolver.go)处理 IPAM block、节点和端点信息，计算需要发送给数据面的路由。[`VXLANResolver`](https://github.com/projectcalico/calico/blob/v3.33.0/felix/calc/vxlan_resolver.go)整理隧道地址、MAC 和节点地址，发出 VTEP 更新；Linux 数据面的 [`vxlanManager`](https://github.com/projectcalico/calico/blob/v3.33.0/felix/dataplane/linux/vxlan_mgr.go)把这些信息转换为内核设备、路由和邻居/FDB 状态。

`tunnelRoute` 有一个值得注意的分支：路由对应的远端 VTEP 尚未到达时，它返回空结果，等待 VTEP 更新再次触发计算。路由信息和节点隧道信息不必同时到达，数据面通过重新协调补齐依赖。排查“IPAM 已分配但远端不通”时，不能只看地址池而不看 VTEP。

```mermaid
sequenceDiagram
    participant C as 客户端 Pod
    participant N1 as 源节点 Linux
    participant U as 节点间网络
    participant N2 as 目标节点 Linux
    participant S as 服务端 Pod
    C->>N1: 内层 IP 包：client Pod IP → remote Pod IP
    N1->>N1: 路由查找：远端网段 → vxlan.calico
    N1->>N1: 邻居表与 FDB：VTEP → 远端节点
    N1->>U: 外层节点 IP → 节点 IP，UDP 4789<br/>携带 VXLAN 与原始业务包
    U->>N2: 按外层节点地址送达
    N2->>N2: VXLAN 解封装，查本地 Pod 路由<br/>执行目标端点 ingress 策略
    N2->>S: 原始 client Pod IP → remote Pod IP
    S-->>N2: 响应包
    N2-->>N1: 反向 VXLAN 转发
    N1-->>C: 响应送入客户端 veth
```

`Always` 表示本地址池的跨节点通信使用 VXLAN，不表示同节点通信也必须封装。VXLAN 封装增加外层 IP、UDP、VXLAN 和内层以太网头；因此要同时核对 Pod 与隧道 MTU，不能只检查节点网卡的 MTU。

BGP 和 VXLAN 也不是两种等价的“隧道协议”：BGP 用来交换路由，VXLAN 用来封装数据。本实验的 VXLAN 集群路由由 Felix 管理，所以禁用了 BIRD。若换成无封装或 IP-in-IP 配置，路由分发与底层可达性条件需要重新分析，不能只删掉图中的 VXLAN 方框。

## Service 在源节点把目的地址改成 Pod IP

客户端访问 ClusterIP 时，Pod 的网络配置没有变化。它仍沿默认路由把包交给宿主机；变化发生在源节点的 Service 规则中。

控制面首先要把“哪些 Pod 是这个 Service 的后端”变成 EndpointSlice。这个工作由 kube-controller-manager 中的 [EndpointSlice 控制器](https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/controller/endpointslice/endpointslice_controller.go)协调：读取 Service 和 Pod 状态，写回 EndpointSlice。它不直接给 kube-proxy 发配置，也不亲自转发请求。

```mermaid
sequenceDiagram
    participant A as API Server
    box kube-controller-manager
        participant E as EndpointSlice 控制器
    end
    participant K as 节点 kube-proxy
    participant L as 节点 Linux
    A-->>E: informer：Service 与 Pod 状态变化
    E->>A: 协调 EndpointSlice
    par 服务定义
        A-->>K: watch：Service
    and 后端集合
        A-->>K: watch：EndpointSlice
    end
    K->>K: 更新 change tracker，合并待处理变化
    K->>K: syncProxyRules 计算规则
    K->>L: iptables-restore 更新 Service 规则
    Note over K,L: 后续数据包由内核转发，不逐包请求 kube-proxy
```

在 [iptables proxier](https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/proxy/iptables/proxier.go) 中，`OnServiceUpdate` 和 `OnEndpointSliceUpdate` 记录变化并触发同步；`syncProxyRules` 在初始 Service 与 EndpointSlice 缓存都完成同步后才生成规则。先等待两类缓存，避免只看到 Service、还没看到已有后端时，就按不完整视图编程。

规则更新也不是每来一个 watch 事件就重建整个数据面。同步 runner 合并变化，`syncProxyRules` 区分局部更新与全量同步；若恢复规则失败，会标记下一次需要全量同步，因为已消费的变化集合不足以继续做可靠的局部更新。

对于普通 ClusterIP，核心关系是：

```text
nat PREROUTING
  → KUBE-SERVICES：匹配 ClusterIP:80
  → KUBE-SVC-...：选择可用后端
  → KUBE-SEP-...：DNAT 到后端 Pod IP:8080
  → 按改写后的目的地址查路由
```

本实验后端只有一个，所以不会观察到多个 endpoint 之间的随机选择。多个后端时，proxier 会为候选 endpoint 生成概率匹配规则；那是新连接进入 NAT 路径时的选择，不是每个 HTTP 请求都重新抽签。

如果被选中的是远端 Pod，DNAT 后的包继续走前一节的 VXLAN 路径。因此抓源节点 `eth0` 上的 VXLAN 包，内层目的地址应该已经是后端 Pod IP 和 8080，不再是 ClusterIP 和 80。

实测抓包确认了这个变化。客户端发往 `10.98.235.247:80` 的请求，离开源节点时已成为：

```text
外层：172.31.0.2:<UDP 源端口> → 172.31.0.3:4789，VNI 4096
内层：10.244.80.65:<TCP 源端口> → 10.244.158.65:8080
HTTP：Host: 10.98.235.247
```

HTTP Host 仍然是应用最初访问的地址。kube-proxy 的这条路径做的是网络层和传输层地址转换，并没有像 HTTP 反向代理那样接收请求后重新组织应用层消息。后端返回的 `peer` 仍为 `10.244.80.65`，说明这次 Pod 到 ClusterIP 的路径只需要 DNAT，没有把客户端地址一并隐藏。

```mermaid
flowchart TB
    A[Pod 发出目标为 ClusterIP:80 的包] --> B[源节点 nat PREROUTING<br/>KUBE-SERVICES → SVC → SEP]
    B --> C[DNAT：目标改成后端 Pod IP:8080]
    C --> D{路由指向哪里？}
    D -->|本地后端| E[目标节点执行端点策略]
    D -->|远端后端| F[vxlan.calico 封装，送往目标节点]
    F --> G[目标节点解封装，查本地 Pod 路由]
    G --> E
    E --> H[经目标 cali 接口送入 Pod 业务进程]
```

这个图突出后端选择与路由的先后关系，具体端点策略还会经过源端 egress、目标端 ingress 等检查，不是只有最后一个方框才可能丢包。宿主机进程自己访问 Service 时，则从本机 OUTPUT 路径进入 Service NAT，不能照搬 Pod 入站到节点的 PREROUTING 入口。

使用 `remote.pod-network.svc.cluster.local` 访问时，还要先完成另一条网络请求：客户端解析器向集群 DNS 发查询，DNS Service 的流量同样经过 Service 转发和 Pod 网络，CoreDNS 返回业务 Service 的 ClusterIP，应用才连接这个地址。本次域名访问也返回 `remote` 和客户端 Pod IP。因此“域名访问不通，但 ClusterIP 通”时，应先拆开 DNS 查询与业务连接，不能把两次访问合成一次失败来解释。

## 返回包如何重新变回 Service 地址

后端监听的是自己的 Pod IP:8080，但客户端认为自己连接的是 ClusterIP:80。两端之所以能维持这个视图，依靠的是 conntrack 记录的连接状态及 NAT 映射。

请求方向建立 DNAT 映射后，同一连接的后续包使用已有状态；返回方向经过相应节点时，内核应用反向转换。客户端最终看到的响应来源恢复为它访问的 Service 地址。只抓目标 Pod 的网卡，看不到“响应源地址是 ClusterIP”，并不表示 Service 转发异常：反向转换还没有在返回路径上完成。

这也解释了两个常见现象。NAT 链的计数不能直接当成所有业务包的计数；修改 Service 后端，也不意味着既有 TCP 连接立即重新选端点。判断规则是否生效时，实验应主动建立新连接，避免把复用连接的结果误判成规则更新失败。

本次 conntrack 中的一条连接同时记录了这两组方向，下面只保留地址与端口字段：

```text
原始方向：src=10.244.80.65 dst=10.98.235.247 sport=45266 dport=80
回复方向：src=10.244.158.65 dst=10.244.80.65 sport=8080 dport=45266
```

两组地址不完全对称正是 DNAT 映射的表现，不是后端回应错了地址。

## Calico 出站 NAT 与 kube-proxy SNAT 各自解决什么

Pod 访问地址池之外的目标时，底层网络未必知道如何把响应送回 Pod 网段。Calico 可以在出站时改写源地址，让响应先回到节点，再按连接状态送回 Pod。

[`makeNATOutgoingRuleIPTables`](https://github.com/projectcalico/calico/blob/v3.33.0/felix/rules/nat.go)的关键条件是：源地址属于需要 masquerade 的地址池，目的地址不属于 Calico 的网络地址池集合。默认动作是 MASQUERADE，配置了专用出站地址时也可以生成 SNAT。判断依据是 IP 池集合，不是“目标是不是公网 IP”；一个集群外私网地址也可以满足出站 NAT 条件。

kube-proxy 的 SNAT 条件则围绕 Service 转发。例如 `externalTrafficPolicy: Cluster` 允许入口节点把外部请求发给其他节点上的后端，proxier 会给这条外部路径打 masquerade 标记，以保证回复经过持有 NAT 状态的路径。另一个特殊场景是后端 Pod 经 Service 访问到自己，也就是 hairpin，endpoint 规则同样会标记 SNAT。

这两类 NAT 要分别查看 `cali-nat-outgoing` 和 `KUBE-MARK-MASQ` / `KUBE-POSTROUTING`。看到后端源地址变化后，不能仅凭“集群装了 Calico”就把所有 SNAT 归到 Calico。

三组实测结果能把这件事区分开：

| 请求 | 服务端看到的源地址 | 对应路径 |
| --- | --- | --- |
| client 访问集群外容器 `172.31.0.5:8080` | `172.31.0.2` | Calico 出站 masquerade，源节点从 eth0 发出 |
| 外部容器访问 `172.31.0.2:30080`，后端在 worker2 | `10.244.80.64` | kube-proxy 为外部流量标记 masquerade，DNAT 后从 vxlan.calico 发出 |
| remote 经 ClusterIP 访问到自己 | `172.31.0.3` | endpoint 的 hairpin masquerade |

第二行尤其容易被“源地址变成 Node IP”这个说法掩盖。这里 `ip route get 10.244.158.65` 选择 `vxlan.calico`，源地址为 `10.244.80.64`，与后端看到的地址一致。kube-proxy 生成的是 MASQUERADE 动作，最终使用什么地址还取决于出接口与内核地址选择，并非把某一个 Node 对象里的地址写死在规则中。

这组 NodePort 结果不能推广到 `externalTrafficPolicy: Local`。本文使用默认 Cluster 策略，Local 对后端选择和源地址保留的影响见 [Service 的流量路径](/posts/kubernetes-service-traffic-path/)。

## NetworkPolicy 限制的是端点流量

Calico 除了提供路由，也把策略转换成端点上的过滤规则。在本配置中，Felix 的 [`WorkloadEndpointToIptablesChains`](https://github.com/projectcalico/calico/blob/v3.33.0/felix/rules/endpoints.go)分别生成去往端点和来自端点的链。网络连通所需的路由存在，不代表策略一定允许包通过。

本文给 `remote` 后端添加一条 ingress 策略，只允许带 `allowed=true` 标签的同命名空间 Pod 访问 TCP 8080。客户端初始不带标签：直接请求后端 Pod IP 与请求 ClusterIP 都应该被阻止；给客户端添加标签后，两条路径都应该恢复。

Service 的 80 已在源节点转换成后端的 8080，目标端点策略判断的是这条实际到达后端的流量。ClusterIP 不提供绕过后端 ingress 策略的捷径。这个结论针对本文的标准数据面与 Pod 间 Service 路径；外部流量经过 SNAT 后，源身份已经改变，需要结合相应流量策略另行判断。

```mermaid
flowchart TB
    A[请求到达 remote 端点] --> B{remote 是否被 ingress 策略选择？}
    B -->|本实验：是| C{源 Pod 标签匹配 allowed=true<br/>且目标为 TCP 8080？}
    C -->|是| D[允许新连接]
    C -->|否| E[丢弃，新连接超时]
    B -->|未被隔离| F[按其他适用规则处理]
```

这里刻意使用新建连接测试。Calico 的端点规则包含连接状态处理，不能拿一个已经建立的连接继续发送数据，就推断后来添加的策略没有生效。

实际结果与路径判断一致：未加标签时，两种访问都超时；同一客户端访问不受该策略选择的 `local` 后端仍然成功。目标节点端点链的“没有策略放行则丢弃”规则计数增加。给 `client` 添加 `allowed=true` 后，Pod IP 与 ClusterIP 访问都恢复，后端再次返回客户端 Pod IP。

这个标签变更没有修改 NetworkPolicy 的端口条件，实际变化出现在策略引用的 IP set：原本为空的源地址集合加入了 `10.244.80.65`。规则用集合表达 selector 当前选中的地址，所以 Pod 标签变化可以通过更新集合改变匹配结果，而不必把所有源地址逐条展开成规则。这是看策略时需要同时检查 iptables 与 IP set 的原因。

## 按断点定位故障

排查时先保留实际五元组和 Pod 所在节点，再沿发生变化的那一层缩小范围。

| 观察到的差异 | 下一步核对的状态 |
| --- | --- |
| 同节点 Pod IP 也不通 | Pod 的 /32 地址和默认路由、veth、宿主机本地路由、端点策略 |
| 同节点通，跨节点不通 | 远端 Pod 网段路由、VTEP、邻居与 FDB、节点之间 UDP 4789、MTU |
| 后端 Pod IP 通，ClusterIP 不通 | Service 端口与 selector、EndpointSlice 的地址与就绪状态、源节点 kube-proxy 规则、是否在测试新连接 |
| ClusterIP 通，域名不通 | DNS 解析链路、CoreDNS 的 Service 与后端、客户端 DNS 配置和 DNS 流量策略 |
| 包已到目标节点但进不了 Pod | 解封装后的目的地址、本地 veth 路由、目标 ingress 策略及丢包计数 |
| Pod 能出站但对端看不到 Pod IP | Calico 出站 NAT 条件、源节点出接口，以及沿途其他设备的 NAT |
| NodePort 后端看到的源地址不同 | externalTrafficPolicy、入口节点、实际出接口和 kube-proxy 的 masquerade 规则 |

一次连接可能同时经过 kube-proxy 的目标地址转换、Calico 的路由与策略，以及内核 conntrack 的返回转换。把 Service 地址、后端地址、隧道地址和节点地址分别记录下来，才能知道下一张表应该查什么。


