
NetworkPolicy 已经创建成功，客户端却仍然能访问服务；把出站流量也限制起来后，直接访问 Pod IP 成功，通过服务名访问却失败。仅检查 YAML 是否提交成功，解释不了这些现象。

本文在 `zj` 的 kind 集群中使用 Calico，依次验证默认访问、入站隔离、白名单、出站隔离和 DNS 放行。每一步都发起新的 HTTP 请求，用实际访问结果判断策略是否生效。

<!--more-->

## 策略对象和策略执行不是一回事

Kubernetes API 接受 NetworkPolicy 对象，实际网络隔离需要支持该能力的网络实现执行。本次使用 Calico v3.31.2，集群配置关闭默认 kindnet。只在 API 中看到策略，不能证明当前 CNI 已经实施它。[官方 NetworkPolicy 文档](https://kubernetes.io/docs/concepts/services-networking/network-policies/)说明了这种依赖。

实验环境为 kind v0.33.0、Kubernetes v1.35.1，在 `dd-network-policy` namespace 中创建三个 Pod：`server` 在 8080 端口提供 HTTP 服务，`good` 和 `other` 作为两个客户端。另有一个 Service 指向 server。

## 没有隔离策略时，两边都能访问

第一步不给三个 Pod 配任何 NetworkPolicy。脚本从 good 和 other 分别请求 server 的 Pod IP，两个请求都成功。

这一步很重要：如果基线已经失败，后续超时可能来自 DNS、Service、应用监听或路由，不能直接归因于刚加入的策略。每次故障实验都应先建立可工作的对照。

## 一个空入站规则会改变什么

然后对 server 创建入站隔离策略：

```yaml
# 选中 server 并启用入站隔离，但没有允许任何入站来源。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-server
spec:
  podSelector:
    matchLabels:
      app: server
  policyTypes:
    - Ingress # 此策略只约束进入 server 的流量。
```

策略应用后，脚本持续尝试新连接，直到 good 的请求失败。这种基于条件的等待避免把“API 已保存”误认为“数据面已经同步完成”。

这里的 `podSelector` 选的是受保护的 Pod，不是访问者。只写 `app: good` 会变成隔离 good 的入站流量，并不会表达“只允许 good 访问 server”。

## 再加允许策略，为什么没有被先前的拒绝覆盖

第二份策略同样选中 server，允许来自 `app=good` 的 TCP 8080 流量。实验观察到 good 恢复访问，other 仍然失败，good 通过 Service ClusterIP 也能访问。

对同一个 Pod 和方向，多份标准 NetworkPolicy 的允许范围取并集。`deny-server` 选中 server 并声明 Ingress，使它进入入站隔离；它没有提供允许来源。`allow-good` 又选中同一 server，贡献了 good 到 TCP 8080 的允许范围。最终可访问集合由全部选中策略共同决定，创建顺序不参与优先级计算。

```yaml
# 以下是 allow-good 的 spec；来源和目标端口在同一条入站规则内。
podSelector:
  matchLabels:
    app: server
policyTypes: [Ingress]
ingress:
  - from:
      - podSelector:
          matchLabels:
            app: good # 这个 selector 默认匹配同 namespace 的来源 Pod。
    ports:
      - protocol: TCP
        port: 8080 # 目标服务实际监听的端口。
```

Calico 还有自己的策略 CRD 和其他能力，本文只验证 Kubernetes `networking.k8s.io/v1` NetworkPolicy，不能把这里的合并规则扩展到所有厂商策略接口。

## server 允许入站，为什么 good 还是访问不了

实验接着给 good 添加空的 Egress 策略。虽然 server 的白名单完全没变，good 的请求仍然失败。

源和目标各自计算允许集合：源未被 Egress 隔离，或源的某条出站规则允许；同时，目标未被 Ingress 隔离，或目标的某条入站规则允许。两端条件都满足，新连接才具备策略上的放行条件。good 被空 Egress 策略选中后，第一项不再满足；增加到 server TCP 8080 的出站允许，Pod IP 请求才恢复。

每条规则内部也有组合关系：多个 `from` 或 `to` 元素提供可选来源或目的地；同一个元素中的 namespaceSelector 与 podSelector 则同时约束同一组 Pod。把两个 selector 写在同一元素，和拆成两个列表元素，会得到不同的允许范围。下面的 DNS 配置使用前一种写法。

这一组对照可以排除“server 的 ingress 规则失效”这个误判。面对连接超时，需要同时检查源 Pod 的 egress 和目标 Pod 的 ingress，而不是只在服务端不断添加允许规则。

## Pod IP 能通，服务名为什么不通

允许 good 访问 server 后，脚本继续用短服务名 `server` 发请求，仍然失败。因为请求需要先完成 DNS 查询，而 good 此时没有到 DNS 服务的出站允许。

随后添加一条规则，同时选择 `kube-system` namespace 和 `k8s-app=kube-dns` 的 Pod，放行 UDP/TCP 53。服务名请求恢复成功。配置中的两个 selector 放在同一个 `to` 元素内，要求它们同时满足：

```yaml
# 这是 DNS 出站允许规则的 egress 部分。
egress:
  - to:
      - namespaceSelector:
          matchLabels:
            kubernetes.io/metadata.name: kube-system
        podSelector:
          matchLabels:
            k8s-app: kube-dns # 与 namespaceSelector 共同约束目的 Pod。
    ports:
      - protocol: UDP
        port: 53
      - protocol: TCP
        port: 53
```

真实集群应先确认 DNS 流量实际发往哪里。启用 NodeLocal DNSCache、使用自定义 DNS 或不同网络实现时，不能直接照抄这组目标 selector。

## 标签选择器怎样变成包过滤条件

数据包里没有 Pod label。执行策略的组件必须先把“哪些 Pod 匹配选择器”转换为数据面可以匹配的地址、端口和方向，再随着 Pod 创建、删除、地址变化更新规则。API Server 保存 NetworkPolicy，只完成了这条链路的输入部分。

以实验使用的 Calico v3.31.2 为例，Felix 的 [`RuleScanner`](https://github.com/projectcalico/calico/blob/v3.31.2/felix/calc/rule_scanner.go#L286)把选择器相关条件转换成 IP set 标识，记录规则引用哪些集合，并通知集合何时开始或停止被使用。在其 [iptables 规则生成路径](https://github.com/projectcalico/calico/blob/v3.31.2/felix/rules/policy.go)中，规则可以引用这些集合，而不必为每个匹配 Pod 展开一份完整策略。这样，成员变化与策略结构变化就能分别处理；这是理解 label 更新为什么也会影响连通性的关键。

不过，API 对象写入成功和节点完成数据面更新仍然隔着观察、计算与下发。本文脚本等待连通性达到预期后才记录结果，不用 apply 成功作为策略已经生效的证据。实验验证的是标准 NetworkPolicy 的访问语义，没有抓取 iptables 规则，也没有比较 Calico 的 eBPF 数据面；上面的实现分析限定在对应源码路径。

标准 NetworkPolicy 采用允许规则并集，也使“默认拒绝”可以和业务允许策略分开维护。空规则策略让被选中的 Pod 进入隔离状态，却不会创建一条能够压过后续允许规则的全局 deny。更换规则顺序无法解决选错 Pod 或漏掉 DNS 的问题，必须检查选择器实际匹配到谁、两个方向分别允许什么。

## 把访问结果放回同一张表

| 实验阶段 | 发起的访问检查 | 实测结果 |
| --- | --- | --- |
| 没有策略 | good 和 other → server Pod IP | 都成功 |
| server 入站隔离 | good → server Pod IP | 失败 |
| 加入 good 白名单 | good、other → Pod IP；good → ClusterIP | good 成功，other 失败；ClusterIP 成功 |
| good 出站隔离 | good → server Pod IP | 失败 |
| 允许 good 到 server | good → Pod IP 和服务名 | Pod IP 成功，服务名失败 |
| 再允许 DNS | good → 服务名 | 成功 |

表格只列脚本实际检查的结果。它没有测量已建立连接在策略更新后何时被切断，也没有覆盖所有 CNI 的 Service NAT 实现；故障定位时应保留客户端、目的地址和连接是否新建这些条件。

实验代码与使用说明见 [k8sdev/deep-dive-network-policy](https://github.com/jimyag/k8sdev/tree/8f9bfbbd44ac061702310f332ee949fcdf0a213d/deep-dive-network-policy)。

