# 数据包在 Kubernetes 中的一生（3）

原文：[Life of a Packet in Kubernetes — Part 3](https://dramasamy.medium.com/life-of-a-packet-in-kubernetes-part-3-dd881476da0f)

作者：[Dinesh Kumar Ramasamy](https://dramasamy.medium.com/)

本章我们会讨论一下 Kubernetes 的 `kube-proxy` 是如何使用 `iptables` 控制流量的。注意，`kube-proxy` + `iptables` 的组合并非完成该任务的唯一选择。

我们会从 Kubernetes 的多种通信模型和实现开始，如果读者已经了解了 Service、ClusterIP 以及 NodePort 的概念，可以直接跳到 `kube-proxy/iptables` 一节。

## Pod 到 Pod

CNI 会配置节点和 Pod 的路由，`kube-proxy` 不会介入 Pod 到 Pod 之间的通信过程。所有的容器都无需 NAT 就能互相通信；节点和容器之间的通信也是无需 NAT 的。

Pod 的 IP 地址是不固定的（也有办法做成静态 IP，但是缺省配置是不提供这种保障的）。在 Pod 重启时 CNI 会给他分配新的 IP 地址，CNI 不负责维护 IP 地址和 Pod 的映射。Pod 名称在 Deployment 之中也是不固定的。

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745924166312/e83ad4b1-8e52-4c6a-816d-7ebf4e744cb7.png align="center")

Deployment 中的 Pod 是无状态的，一个应用可能会有多个 Pod 副本，因此需要一个负载均衡之类的东西来负责对外开放服务，Kubernetes 中的 Service 对象负责完成这个任务。

## Pod 到外部

Kubernetes 会使用 [SNAT](https://en.wikipedia.org/wiki/Network_address_translation) 完成从 Pod 向外发出的访问。SNAT 会将 Pod 的内部 `IP:Port` 替换为主机的 `IP:Port`。返回数据包到达节点时，`IP:Port` 又会换回 Pod。这个过程对于原始 Pod 是透明无感知的。

## Pod 到 Service

### Cluster IP

Kubernetes 有一个叫做 `Service` 的对象，是一个通向 Pod 的 4 层负载均衡。`Service` 对象有很多类型，最基本的类型叫做 `ClusterIP`，这种类型的 Service 有一个唯一的 VIP 地址，其路由范围仅在集群内部有效。

Kubernetes 集群中，Pod 可能发生移动、重启、升级或者扩缩容，因此向应用 Pod 发送流量是有困难的，另外应用通常有多个副本，我们需要一些方法来进行负载均衡。

Kubernetes 使用 `Service` 对象来解决这个问题。Service 是一个 API 对象，它用一个虚拟 IP 映射到一组 Pod。另外 Kubernetes 为每个 Service 的名称及其虚拟 IP 建立了 DNS 记录，因此可以轻松地根据名称进行寻址。

虚拟 IP 到 Pod IP 的转换是通过每个节点上的 `kube-proxy` 完成的。在 Pod 向外发起通信时，这个进程会通过 iptables 或者 IPVS 自动把 VIP 转为 Pod IP，每个连接都有跟踪，所以数据包返回时候，地址还能够被正确地转回原样。IPVS 和 iptables 在 VIP 和 Pod IP 之间承担着负载均衡的角色，IPVS 能够提供更多的负载均衡算法。虚拟 IP 并不存在于网络接口上，而是在 iptable 中：

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745924179350/6bbfaf38-6bc4-44ac-9327-ad96ab989059.png align="center")

FrontEnd Deployment：

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp
  labels:
    app: webapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: webapp
  template:
    metadata:
      labels:
        app: webapp
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
```

Backend Deployment：

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: auth
  labels:
    app: auth
spec:
  replicas: 2
  selector:
    matchLabels:
      app: auth
  template:
    metadata:
      labels:
        app: auth
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
```

Service：

```yaml
---
apiVersion: v1
kind: Service
metadata:
  name: frontend
  labels:
    app: frontend
spec:
  ports:
  - port: 80
    protocol: TCP
  type: ClusterIP
  selector:
    app: webapp
---
apiVersion: v1
kind: Service
metadata:
  name: backend
  labels:
    app: backend
spec:
  ports:
  - port: 80
    protocol: TCP
  type: ClusterIP
  selector:
    app: auth
...
```

现在 FrontEnd Pod 能够通过 ClusterIP 或者 DNS 名称来访问 Backend 了。CoreDNS 这样的 DNS 服务器具备 Kubernetes 集群感知的能力，他们会对 Kubernetes API 进行监控，一旦新建了 Service，就会新建对应的 DNS 记录。如果集群中启用的 DNS，所有 Pod 都能够自动的根据 DNS 名称来解析到 Service。

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745924189708/c20b4cf7-49f0-47dd-bfbb-2a2aa0774d4d.png align="center")

### NodePort（外部到 Pod）

在集群内部可以用 DNS 访问 Service。然而 Service 的 IP 是私有的和虚拟的，所以集群外是无法访问的。

试试看从外部访问 frontEnd 的 Pod（此时还没有给 frontEnd 创建 Service）：

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745924204972/68a7b429-3d41-4223-9d34-3a8c99860bb7.png align="center")

Pod IP 是私有的，无法路由。

接下来创建一个 NodePort 类型的 Service 把 FrontEnd 服务开放给外部世界。如果把 `type` 字段设置为 `NodePort`，Kubernetes 控制面使用 `--service-node-port-range` 参数为 NodePort 服务分配了一个端口范围。每个节点都会会把这个端口映射给特定的服务。Service 使用 `.spec.ports[*].nodePort` 字段来指定该端口：

```yaml
---
apiVersion: v1
kind: Service
metadata:
  name: frontend
spec:
  type: NodePort
  selector:
    app: webapp
  ports:
      # By default and for convenience, the `targetPort` is set to the same value as the `port` field.
    - port: 80
      targetPort: 80
      # Optional field
      # By default and for convenience, the Kubernetes control plane will allocate a port from a range (default: 30000-32767)
      nodePort: 31380
...
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745924215574/77bf1d0d-e30b-4b45-ac3d-b7910bce64e3.png align="center")

这样就可以在集群外使用任意节点的 nodePort 来访问服务了。还可以给 `nodePort` 赋值以指定特定开放端口。这种情况下，为了防止端口冲突，需要自行管理端口，并且指定端口也必须在参数中声明的端口范围之内。

### ExternalTrafficPolicy

`ExternalTrafficPolicy` 字段表明所属 Service 对象会把来自外部的流量路由给本节点还是集群范围内的端点。如果赋值为 `Local`，会保留客户端源 IP 同时避免 `NodePort` 类型服务的多余一跳，但是有流量分配不均匀的隐患；如果设置为 `Cluster`，会抹掉客户端的源 IP，并导致到其它节点的一跳，但会获得相对较好的均衡效果。

#### Cluster

这是 Kubernetes Service 的缺省 `ExternalTrafficPolicy`。这个选项会把流量平均分配给该 Service 的所有 Pod 上。

这种策略的一个弱点是会存在不必要的节点间网络跳转。例如在一个节点的 NodePort 上接收到流量时，即使本节点上存在可用 Pod，流量还是可能会随机地把流量路由到另外一个节点上的 Pod，造成不必要的跳转。

在 `Cluster` 策略下，数据包的流向：

* 客户端把数据包发送给 `node2:31380`；
    
* `node2` 替换源 IP 地址（SNAT）为自己的 IP 地址；
    
* `node2` 将目标地址替换为 Pod IP；
    
* 数据包被路由到 `node1` 或者 `node3`，然后到达 Pod；
    
* Pod 的响应返回到 `node2`；
    
* Pod 的响应返回到客户端。
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745924226308/1c9f2a14-8f41-460e-9672-69a4ebfb7a1a.png align="center")

#### Local

这种策略中，`kube-proxy` **只会在存在目标 Pod 的节点上**加入 NodePort 的代理规则。API Server 要求只有使用 `LoadBalancer` 或者 `NodePort` 类型的 Service 才能够使用这种策略。这是因为 `Local` 策略只跟外部访问相关。

如果使用了 `Local` 策略，`kube-proxy` 只会代理到本地 `endpoint` 的流量，不会向其它节点转发。如果本地没有相应端点，发送到该节点的流量就会被丢弃，所以数据包中会保留正确的源 IP，可以放心的在数据包处理规则中使用。

```yaml
---
apiVersion: v1
kind: Service
metadata:
  name: frontend
spec:
  type: NodePort
  externalTrafficPolicy: Local
  selector:
    app: webapp
  ports:
      # By default and for convenience, the `targetPort` is set to the same value as the `port` field.
    - port: 80
      targetPort: 80
      # Optional field
      # By default and for convenience, the Kubernetes control plane will allocate a port from a range (default: 30000-32767)
      nodePort: 31380
...
```

`Local` 策略下的数据包：

* 客户端发送数据包到 `node1:31380`，这个端点上存在目标 Pod；
    
* `node1` 把数据包路由到端点，其中带有正确的源 IP；
    
* 因为策略限制，`node1` 不会把数据包发给 `node3`；
    
* 客户端发送数据包给 `node2:31380`，该节点上不存在目标 Pod；
    
* 数据包被丢弃。
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745924235720/146fc64b-592d-4287-af32-8c5eb505feca.png align="center")

#### LoadBalancer Service 类型中的 Local 策略

如果在 Google GKE 上使用 `Local` 策略，由于健康检查的原因，会把不运行对应 Pod 的节点从负载均衡池中剔除，所以不会发生丢弃流量的问题。这种模型对于需要处理大量外部入栈流量，需要避免跨节点跳转从而降低延迟的应用非常有帮助。另外因为不需要进行 SNAT，从而让源 IP 得以保存。然而官方文档声明，这种策略存在不够均衡的短板。

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745924244768/7c3fe28b-a209-497e-8261-6fe7a948294a.png align="center")

## Kube-Proxy（iptable）

Kubernetes 中负责 Service 对象的组件就是 `kube-proxy`。它在每个节点上运行，为 Pod 和 Service 生成复杂的 iptables 规则，完成所有的过滤和 NAT 工作。如果登录到 Kubernetes 节点上，运行 `iptables-save`，会看到 Kubernetes 或者其它组件生成的规则。最重要的是 `KUBE-SERVICE`、`KUBE-SVC-*` 以及 `KUBE-SEP-*`：

* `KUBE-SERVICE` 是 `Service` 包的入口。它负责匹配 IP:Port，并把数据包发给对应的 `KUBE-SVC-*`。
    
* `KUBE-SVC-*` 担任负载均衡的角色，会平均分配数据包到 `KUBE-SEP-*`。每个 `KUBE-SVC-*` 都有和 `Endpoint` 同样数量的 `KUBE-SEP-*`。
    
* `KUBE-SEP-*` 代表的是 `Service` 的 `EndPoint`，它负责的是 DNAT，会把 `Service` 的 IP:Port 替换为 Pod 的 IP:Port。
    

Conntrack 会介入 DNAT 过程，使用状态机来跟踪连接状态。为了记住目标地址的变更，并在回包时候进行恢复，这些状态是必须保存的。iptables 还可以根据 conntrack 状态（ctstate）来决定数据包的目标。下面四个 conntrack 状态尤其重要：

* `NEW`：conntrack 对该数据包一无所知，该状态出现在收到 `SYN` 的时候。
    
* `ESTABLISHED`：conntrack 知道该数据包属于一个已发布连接，该状态出现于握手完成之后。
    
* `RELATED`：这个数据包不属于任何连接，但是他是隶属于其它连接的，在 FTP 之类的协议里常用。
    
* `INVALID`：有问题的数据包，conntrack 不知道如何处理。这种状态是 Kubernetes 问题的常客。
    

Service 和 Pod 之间的 TCP 连接过程如下：

* 左侧的客户端 Pod 发送数据包到一个 Service：`2.2.2.10:80`；
    
* 数据包经过客户端节点的 iptables 规则，目标改为 `1.1.1.20:80`；
    
* 服务端 Pod 处理数据包，发送一个响应包到 `1.1.1.10`；
    
* 数据包回到客户端节点，conntrack 认出这个数据包，把源地址改回 `2.2.2.10:80`；
    
* 客户端 Pod 收到响应包。
    

## iptables

在 Linux 操作系统中使用 `netfilter` 处理防火墙工作。这是一个内核模块，决定是否放行数据包。iptables 是 netfilter 的前端。二者经常被混为一谈。

### 链

每条链负责一种特定任务。

* `PREROUTING`：决定数据包刚刚进入网络端口时的对策。有几种不同的选择，例如修改数据包（NAT），丢弃数据包或者什么都不做使其通过；
    
* `INPUT`：其中经常包含一些用于防止恶意行为的严格规则，防止系统遭到入侵。开放或者屏蔽端口的行为就是在这里进行的；
    
* `FORWARD`：顾名思义，负责数据包的转发。在将服务器作为路由器的时候，就需要在这里完成任务。
    
* `OUTPUT`：这里负责所有的网络浏览的行为。这里可以限制所有数据包的发送。
    
* `POSTROUTING`：发生在数据包离开服务器之前，数据包最后的可跟踪位置。
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745924257957/5a0b6650-ac25-4219-8425-01a6044d8c9f.png align="center")

`FORWARD` 仅在 `ip_forward` 启用时才有效。所以下面的命令在 Kubernetes 中很重要：

```bash
$ sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1
$ cat /proc/sys/net/ipv4/ip_forward
1
```

上面的变更是暂时性的，要持久化这个变更，需要在 `/etc/sysctl.conf` 中写入 `net.ipv4.ip_forward = 1`。

### 表

接下来会讨论 NAT 表，除此之外还有几个：

* `Filter`：缺省表，这里决定是否允许数据包出入本机，因此可以在这里进行屏蔽等操作；
    
* `Nat`：是网络地址转换的缩写。下面会有例子说明；
    
* `Mangle`：仅对特定包有用。它的功能是在包出入之前修改包中的内容；
    
* `RAW`：用于处理原始数据包，主要用在跟踪连接状态，下面有一个放行 SSH 连接的例子。
    
* `Security`：负责在 Filter 之后保障安全。
    

### Kubernetes 中的 iptables 配置

部署一个 2 副本 Nginx 应用，导出 iptables 规则。

> 服务类型 NodePort

```bash
$ kubectl get svc webapp
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
webapp NodePort 10.103.46.104 <none> 80:31380/TCP 3d13h
$ kubectl get ep webapp 
NAME ENDPOINTS AGE
webapp 10.244.120.102:80,10.244.120.103:80 3d13h
```

ClusterIP 是一个存在于 iptables 中的虚拟 IP，Kubernetes 会把这个地址存在 CoreDNS 中。

```bash
$ kubectl exec -i -t dnsutils -- nslookup webapp.default
Server:  10.96.0.10
Address: 10.96.0.10#53
Name: webapp.default.svc.cluster.local
Address: 10.103.46.104
```

为了能够进行包过滤和 NAT，Kubernetes 会创建一个 `KUBE-SERVICES` 链，把所有 `PREROUTING` 和 `OUTPUT` 流量转发给 `KUBE-SERVICES`：

```bash
sudo iptables -t nat -L PREROUTING | column -t
Chain            PREROUTING  (policy  ACCEPT)                                                                    
target           prot        opt      source    destination                                                      
cali-PREROUTING  all         --       anywhere  anywhere     /*        cali:6gwbT8clXdHdC1b1  */                 
KUBE-SERVICES    all         --       anywhere  anywhere     /*        kubernetes             service   portals  */
DOCKER           all         --       anywhere  anywhere     ADDRTYPE  match                  dst-type  LOCAL
```

使用 `KUBE-SERVICES` 介入包过滤和 NAT 之后，Kubernetes 会监控通向 Service 的流量，并进行 SNAT/DNAT 的处理。在 `KUBE-SERVICES` 链尾部，会写入另一个链 `KUBE-SERVICES`，用于处理 `NodePort` 类型的 Service。

`KUBE-SVC-2IRACUALRELARSND` 链会处理针对 `ClusterIP` 的流量，否则的话就会进入 `KUBE-NODEPORTS`：

```bash
$ sudo iptables -t nat -L KUBE-SERVICES | column -t
Chain                      KUBE-SERVICES  (2   references)                                                                                                                                                                             
target                     prot           opt  source          destination                                                                                                                                                             
KUBE-MARK-MASQ             tcp            --   !10.244.0.0/16  10.103.46.104   /*  default/webapp                   cluster  IP          */     tcp   dpt:www                                                                          
KUBE-SVC-2IRACUALRELARSND  tcp            --   anywhere        10.103.46.104   /*  default/webapp                   cluster  IP          */     tcp   dpt:www                                                                                                                                             
KUBE-NODEPORTS             all            --   anywhere        anywhere        /*  kubernetes                       service  nodeports;  NOTE:  this  must        be  the  last  rule  in  this  chain  */  ADDRTYPE  match  dst-type  LOCAL
```

看看 `KUBE-NODEPORTS` 的内容：

```bash
$ sudo iptables -t nat -L KUBE-NODEPORTS | column -t
Chain                      KUBE-NODEPORTS  (1   references)                                            
target                     prot            opt  source       destination                               
KUBE-MARK-MASQ             tcp             --   anywhere     anywhere     /*  default/webapp  */  tcp  dpt:31380
KUBE-SVC-2IRACUALRELARSND  tcp             --   anywhere     anywhere     /*  default/webapp  */  tcp  dpt:31380
```

看起来 `ClusterIP` 和 `NodePort` 处理过程是一样的，那么看看下面的处理流程：

```bash
# statistic  mode  random -> Random load-balancing between endpoints.
$ sudo iptables -t nat -L KUBE-SVC-2IRACUALRELARSND | column -t
Chain                      KUBE-SVC-2IRACUALRELARSND  (2   references)                                                                             
target                     prot                       opt  source       destination                                                                
KUBE-SEP-AO6KYGU752IZFEZ4  all                        --   anywhere     anywhere     /*  default/webapp  */  statistic  mode  random  probability  0.50000000000
KUBE-SEP-PJFBSHHDX4VZAOXM  all                        --   anywhere     anywhere     /*  default/webapp  */

$ sudo iptables -t nat -L KUBE-SEP-AO6KYGU752IZFEZ4 | column -t
Chain           KUBE-SEP-AO6KYGU752IZFEZ4  (1   references)                                               
target          prot                       opt  source          destination                               
KUBE-MARK-MASQ  all                        --   10.244.120.102  anywhere     /*  default/webapp  */       
DNAT            tcp                        --   anywhere        anywhere     /*  default/webapp  */  tcp  to:10.244.120.102:80

$ sudo iptables -t nat -L KUBE-SEP-PJFBSHHDX4VZAOXM | column -t
Chain           KUBE-SEP-PJFBSHHDX4VZAOXM  (1   references)                                               
target          prot                       opt  source          destination                               
KUBE-MARK-MASQ  all                        --   10.244.120.103  anywhere     /*  default/webapp  */       
DNAT            tcp                        --   anywhere        anywhere     /*  default/webapp  */  tcp  to:10.244.120.103:80

$ sudo iptables -t nat -L KUBE-MARK-MASQ | column -t
Chain   KUBE-MARK-MASQ  (24  references)                         
target  prot            opt  source       destination            
MARK    all             --   anywhere     anywhere     MARK  or  0x4000
```

> 注意：输出内容已经被精简。

* ClusterIP：`KUBE-SERVICES` → `KUBE-SVC-XXX` → `KUBE-SEP-XXX`
    
* NodePort：`KUBE-SERVICES` → `KUBE-NODEPORTS` → `KUBE-SVC-XXX` → `KUBE-SEP-XXX`
    

> NodePort 服务会有一个 ClusterIP 用于处理内外部通信。

上述规则的可视化表达：

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745924271586/04c8c07f-453b-4da4-a843-ef2497dac417.png align="center")

### ExtrenalTrafficPolicy: Local

如前文所述，使用 `ExtrenalTrafficPolicy: Local` 会保留源 IP，并在到达节点上没有 Endpoint 的时候丢弃流量。没有本地 Endpoint 的节点上，iptables 的规则会怎样？

使用 `ExtrenalTrafficPolicy: Local` 部署 Nginx 服务：

```bash
$ kubectl get svc webapp -o wide -o jsonpath={.spec.externalTrafficPolicy}
Local

$ kubectl get svc webapp -o wide
NAME     TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE   SELECTOR
webapp   NodePort   10.111.243.62   <none>        80:30080/TCP   29m   app=webserver
```

检查一下没有本地 Endpoint 的节点上的 iptables 规则：

```bash
$ sudo iptables -t nat -L KUBE-NODEPORTS
Chain KUBE-NODEPORTS (1 references)
target prot opt source destination
KUBE-MARK-MASQ tcp — 127.0.0.0/8 anywhere /* default/webapp */ tcp dpt:30080
KUBE-XLB-2IRACUALRELARSND tcp — anywhere anywhere /* default/webapp */ tcp dpt:30080
```

再看一下 `KUBE-XLB-2IRACUALRELARSND`：

```bash
$ iptables -t nat -L KUBE-XLB-2IRACUALRELARSND
Chain KUBE-XLB-2IRACUALRELARSND (1 references)
target prot opt source destination
KUBE-SVC-2IRACUALRELARSND all — 10.244.0.0/16 anywhere /* Redirect pods trying to reach external loadbalancer VIP to clusterIP */
KUBE-MARK-MASQ all — anywhere anywhere /* masquerade LOCAL traffic for default/webapp LB IP */ ADDRTYPE match src-type LOCAL
KUBE-SVC-2IRACUALRELARSND all — anywhere anywhere /* route LOCAL traffic for default/webapp LB IP to service chain */ ADDRTYPE match src-type LOCAL
KUBE-MARK-DROP all — anywhere anywhere /* default/webapp has no local endpoints */
```

这里就会看到，集群级别的流量没什么问题，但是 NodePort 流量会被丢弃。

## Headless Service

有的应用并不需要负载均衡和服务 IP。在这种情况下就可以使用 `headless` Service，只要设置 `.spec.clusterIP` 为 `None` 即可。

可以借助这种服务类型和其他服务发现机制协作，无需和 Kubernetes 绑定。`kube-proxy` 不对这种没有 IP 的服务提供支持，也就没有什么负载均衡和代理之类的能力了。DNS 的配置要根据 Selector 来确定。

### 有 Selector

定义了 Selector 的 Headless Service，Endpoint 控制器会创建 `Endpoint` 记录，并修改 DNS 记录来直接返回 Service 后端的 Pod 地址。

```bash
$ kubectl get svc webapp-hs
NAME        TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
webapp-hs   ClusterIP   None         <none>        80/TCP    24s
$ kubectl get ep webapp-hs
NAME        ENDPOINTS                             AGE
webapp-hs   10.244.120.109:80,10.244.120.110:80   31s
```

### 无 Selector

没有定义 Selector 的 Headless Service，也就没有 Endpoint 记录。然而 DNS 系统会尝试配置：

* `ExternalName` 类型的服务，会产生 `CNAME` 记录；
    
* 其他类型则是所有 Endpoint 共享服务名称。
    

如果外部 IP 被路由到集群节点上，Kubernetes Service 可以用 `externalIPs` 开放出来。通过 `externalIP` 进入集群的流量，会被路由到 Service Endpoint 上。`externalIPs` 不是 Kubernetes 管理的，需要集群管理员自行维护。

## 网络策略

阅读至此，Kubernetes 网络策略的实现方法已经呼之欲出了——是的，就是 iptables。目前是 CNI 而非 `kube-proxy` 负责实现网络策略。这部分内容本来应该写在第二篇 Calico 的内容里，然而我认为这里写出来可能更合适。

我们创建三个服务：frontend、backend 和 db。

缺省情况下，Pod 没有任何隔离，会接受任何来源的通信。

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745924291420/4503b1f4-75ea-4675-9c19-3f47d13b8b28.png align="center")

想要制定规则，禁止 frontend 访问 db：

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745924298193/54ce5444-ec7a-4276-86ce-3685e6f228fc.png align="center")

这里推荐阅读 [Guide to Kubernetes Ingress Network Policies](https://www.openshift.com/blog/guide-to-kubernetes-ingress-network-policies) 了解网络策略配置方面的更多内容。本节内容关注的是 Kubernetes 中策略的实现方式，而非配置知识。

创建一个策略把 db 和 frontend 隔离开，这样一来 frontend 和 db 之间的流量就会被阻断。

> 上图中为了简单起见，写的是 Service 而非 Pod，安全策略的控制对象实际上是 Pod。

策略实施之后会产生如下效果，frontend 的 Pod 能访问 backend 但是无法访问 db。backend 的 Pod 可以访问 db。

```bash
$ kubectl exec -it frontend-8b474f47-zdqdv -- /bin/sh
$ curl backend
backend-867fd6dff-mjf92
$ curl db
curl: (7) Failed to connect to db port 80: Connection timed out

$ kubectl exec -it backend-867fd6dff-mjf92 -- /bin/sh
$ curl db
db-8d66ff5f7-bp6kf
```

看看这里用到的网络策略：只允许 `‘allow-db-access` 标签设置为 `true` 的 Pod 访问 db。

Calico 会把 Kubernetes 网络策略翻译成 Calico 格式：

```bash
$ calicoctl get networkPolicy --output yaml
apiVersion: projectcalico.org/v3
items:
- apiVersion: projectcalico.org/v3
  kind: NetworkPolicy
  metadata:
    creationTimestamp: "2020-11-05T05:26:27Z"
    name: knp.default.allow-db-access
    namespace: default
    resourceVersion: /53872
    uid: 1b3eb093-b1a8-4429-a77d-a9a054a6ae90
  spec:
    ingress:
    - action: Allow
      destination: {}
      source:
        selector: projectcalico.org/orchestrator == 'k8s' && networking/allow-db-access
          == 'true'
    order: 1000
    selector: projectcalico.org/orchestrator == 'k8s' && app == 'db'
    types:
    - Ingress
kind: NetworkPolicyList
metadata:
  resourceVersion: 56821/56821
```

iptables 的 `filter` 表在网络策略的实现中起了很重要的作用。Calico 中用到了 `ipsec` 等高级概念，难于进行反向工程。在这个规则中可以看到，只有来自 backend 的流量才被允许发给 db。

使用 `calicoctl` 获取 endpoint 详情：

```bash
$ calicoctl get workloadEndpoint
WORKLOAD                         NODE       NETWORKS        INTERFACE         
backend-867fd6dff-mjf92          minikube   10.88.0.27/32   cali2b1490aa46a   
db-8d66ff5f7-bp6kf               minikube   10.88.0.26/32   cali95aa86cbb2a   
frontend-8b474f47-zdqdv          minikube   10.88.0.24/32   cali505cfbeac50
```

`cali95aa86cbb2a` 就是 db Pod veth 的主机侧。

看看跟这个网络接口有关的 iptables 规则：

```bash
$ sudo iptables-save | grep cali95aa86cbb2a
:cali-fw-cali95aa86cbb2a - [0:0]
:cali-tw-cali95aa86cbb2a - [0:0]
...
-A cali-tw-cali95aa86cbb2a -m comment --comment "cali:pm-LK-c1ra31tRwz" -m mark --mark 0x0/0x20000 -j cali-pi-_tTE-E7yY40ogArNVgKt
-A cali-tw-cali95aa86cbb2a -m comment --comment "cali:q_zG8dAujKUIBe0Q" -m comment --comment "Return if policy accepted" -m mark --mark 0x10000/0x10000 -j RETURN
-A cali-tw-cali95aa86cbb2a -m comment --comment "cali:FUDVBYh1Yr6tVRgq" -m comment --comment "Drop if no policies passed packet" -m mark --mark 0x0/0x20000 -j DROP
-A cali-tw-cali95aa86cbb2a -m comment --comment "cali:X19Z-Pa0qidaNsMH" -j cali-pri-kns.default
-A cali-tw-cali95aa86cbb2a -m comment --comment "cali:Ljj0xNidsduxDGUb" -m comment --comment "Return if profile accepted" -m mark --mark 0x10000/0x10000 -j RETURN
-A cali-tw-cali95aa86cbb2a -m comment --comment "cali:0z9RRvvZI9Gud0Wv" -j cali-pri-ksa.default.default
-A cali-tw-cali95aa86cbb2a -m comment --comment "cali:pNCpK-SOYelSULC1" -m comment --comment "Return if profile accepted" -m mark --mark 0x10000/0x10000 -j RETURN
-A cali-tw-cali95aa86cbb2a -m comment --comment "cali:sMkvrxvxj13WlTMK" -m comment --comment "Drop if no profiles matched" -j DROP
$ sudo iptables-save -t filter | grep cali-pi-_tTE-E7yY40ogArNVgKt
:cali-pi-_tTE-E7yY40ogArNVgKt - [0:0]
-A cali-pi-_tTE-E7yY40ogArNVgKt -m comment --comment "cali:M4Und37HGrw6jUk8" -m set --match-set cali40s:LrVD8vMIGQDyv8Y7sPFB1Ge src -j MARK --set-xmark 0x10000/0x10000
-A cali-pi-_tTE-E7yY40ogArNVgKt -m comment --comment "cali:sEnlfZagUFRSPRoe" -m mark --mark 0x10000/0x10000 -j RETURN
```

检查一下 ipset，会看到只有来自 backend pod 的 `10.88.0.27` 才能访问 db。
