# 研究 k8s 网络工作原理

## 背景

用 k8s 也有一段时间了，之前遇到过 iptables 等出现问题，导致 k8s 节点间网络出现问题，于是想研究一下 k8s 的网络工作原理。

## Docker 网络

首先研究一下 Docker 网络连接是如何实现的。Docker 首先会创建一个 bridge，名为 bridge0:

```shell
$ ip a show docker0
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:c4:87:73:bf brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:c4ff:fe87:73bf/64 scope link
       valid_lft forever preferred_lft forever
```

默认情况下，每个容器都会有单独的一个 netns，然后创建一对 veth pair，一端留在 global netns，另一端放到容器中。在 global netns 中的 veth 端口会加入到 docker0 中：

```shell
$ ip a show dev veth3db9316
21: veth3db9316@if20: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
    link/ether e2:49:a6:2d:5a:bd brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::e049:a6ff:fe2d:5abd/64 scope link
       valid_lft forever preferred_lft forever
$ brctl show docker0
bridge name     bridge id               STP enabled     interfaces
docker0         8000.0242c48773bf       no              veth3db9316
```

容器中的网络，在 veth 上 docker 会分配并配置一个地址（比如 172.17.0.2），然后设置默认路由 via 172.17.0.1。一方面，可以通过默认路由到 172.17.0.1 再通过 iptables NAT 访问外面的网络：

```shell
$ iptables-save -t nat
# Generated by xtables-save v1.8.2 on Sat Sep 18 10:44:49 2021
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:DOCKER - [0:0]
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A DOCKER -i docker0 -j RETURN
COMMIT
# Completed on Sat Sep 18 10:44:49 2021
```

另一方面，因为连接不同容器的 veth 在同一个 bridge 下面，所以不同容器的可以认为在同一个二层网络中，自然可以互相访问。

## K8s 网络

在 k8s 中，所有的 pod 都希望可以通过 IP 地址互联。一个思路是把各个节点上的 pod 通过类似 docker 的方法实现，即每个 netns 通过 veth 连接到一个 bridge 上，然后再想办法去路由在其它节点上的 pod。

因为我用 k3s 搭建 k8s 集群，它用的 cni 是 flannel。flannel 采用的是 vxlan 的方式来实现节点间的网络通信。

首先还是看看节点内的 pod 如何组网。

```shell
5: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default qlen 1000
    link/ether 6a:4f:ff:8b:b1:b3 brd ff:ff:ff:ff:ff:ff
    inet 10.42.0.1/24 brd 10.42.0.255 scope global cni0
       valid_lft forever preferred_lft forever
    inet6 fe80::7cf6:57ff:fed7:c49b/64 scope link
       valid_lft forever preferred_lft forever
6: vethc47d6140@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP group default
    link/ether da:19:f8:48:f6:49 brd ff:ff:ff:ff:ff:ff link-netns cni-9d2a5120-16a3-453e-bf64-c4006c06c93b
    inet6 fe80::d819:f8ff:fe48:f649/64 scope link
       valid_lft forever preferred_lft forever
```

首先，flannel 给每个节点分配了一个 /24 的网段，比如第一个节点是 10.42.0.0/24，第二个是 10.42.1.0/24，依次类推。然后，节点内的 pod 就从这个网段里分配地址，比如 10.42.0.50/24，它的默认网关是 10.42.0.1。这些 veth 都会加入到 cni0 的 bridge 中。这一部分原理和 docker 是一样的，只不过名字不同了。也有相应的 iptables 规则：

```shell
$ iptables-save | grep MASQUERADE
-A POSTROUTING -s 10.42.0.0/16 ! -d 224.0.0.0/4 -j MASQUERADE --random-fully
-A POSTROUTING ! -s 10.42.0.0/16 -d 10.42.0.0/16 -j MASQUERADE --random-fully
```

那么，节点间网络如何实现呢？假如，我们要从第一个节点 pod 10.42.0.50/24 访问第二个节点的 pod 10.42.1.51/24，首先，pod 根据默认路由会发给 10.42.0.1/24，到达第一个节点的 cni0，然后查路由表：

```shell
$ ip r
10.42.0.0/24 dev cni0 proto kernel scope link src 10.42.0.1
10.42.1.0/24 via 10.42.1.0 dev flannel.1 onlink
```

可以看到，它会匹配 10.42.1.0/24 via 10.42.1.0 dev flannel.1 的路由。flannel.1 是一个 vxlan 的 interface：

```shell
$ ip a show flannel.1
4: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default
    link/ether b6:2f:39:4a:02:c0 brd ff:ff:ff:ff:ff:ff
    inet 10.42.0.0/32 scope global flannel.1
       valid_lft forever preferred_lft forever
    inet6 fe80::b42f:39ff:fe4a:2c0/64 scope link
       valid_lft forever preferred_lft forever
```

当这个 interface 接收到一个 packet 的时候，会查询 fdb：

```shell
$ bridge fdb show brport flannel.1
...
```

这个 fdb 中包括了 (MAC 地址，IP 地址) 的 tuple。当 flannel.1 收到一个 Ethernet Frame 的时候，如果目的地址匹配这里的 MAC 地址，就会直接把 Eth Frame 封装到 UDP 里面发给目的 IP 地址；否则，就会在这个表里面 broadcast。这样，第二个节点就会收到 packet 并且转给实际的 pod。

## 总结

总结一下 k8s 的网络互联的实现方法：节点内通过 bridge 实现，把链接各个 netns 的 veth 桥接起来；节点间划分为多个子网，子网间通过 flannel 的网关进行路由，flannel 网关间通过 vxlan 进行互联。

## 参考文档

[技术干货 | 深入理解 flannel](https://zhuanlan.zhihu.com/p/34749675)

[一文看懂 k8s 的 Flannel 网络](https://zhuanlan.zhihu.com/p/140711132)
