*本文是​​Envoy透明流量劫持和路由转发(Calico网络)学习笔记​​的后续笔记

CNI在K8s全景中的位置:

——k8s的CRI(container runtime interface)规范旨在定义 k8s 如何更好地操作容器技术,该规范大概分为三部分:CRI Client(kubelet),CRI Server(Containerd),OCI Runtime( runc)。

​—— OCI 规范中有一步叫 “创建网络”:该规范让 CRI 调用这个插件(自由实现不同的 CNI 插件),并把容器的运行时信息,包括容器的命名空间,容器 ID 等信息传给插件,插件虽然实现的方法各式各样,但是最终目的只有一个,就是让集群中的各种 pod 之间能自由通信。

——首先 kubelet 会启动 CRI Runtime(container runtime interface),这个简单来讲就是在 kubelet 端启动一个 grpc 的客户端,然后需要有个对应的 CRI Server 端,这里一般实现就是 containerd,也就是说 containerd 会启动一个 grpc 的服务端,接收来自 kubelet 的 “创建 Pod” 的请求。在接到这个请求后,containerd 会调用“创建 Sandbox”的接口,所谓创建 sandbox 就是给 Pod 中的容器们提前启动一个具有稳定网络资源以及存储资源的隐藏 pause 容器(在 k8s 里,挂载存储功能由 CSI 实现,挂载网络由 CNI 实现)。但是 containerd 属于“高级运行时”真正去拉起容器的地方在 OCI 中,也叫“低级运行时”,一般实现可能有 runc 或者 kata 等,最常用的可能就是 runc。{containerd 基本上只负责调用(高级运行时),真正实现这些功能的地方在 OCI 的 runc(或其他低级运行时)中;containerd 到 OCI 之间还会有一层 shim,containerd 到 runc 有 runc-shim,到 kata 有 kata-shim},然后由于为了给 sandbox 创建网络资源,会先去 /etc/cni/net.d 目录下加载网络配置文件,然后根据配置文件中的 type 字段,去 /opt/cni/bin 目录下找对应的二进制插件,随后把容器的运行时信息作为环境变量,再把配置文件作为标准输出,对这个二进制进行调用。二进制插件调用的过程主要就是要实现三个点:

  1. pod IP地址管理:该功能在calico或者flannel的实现中,都是通过自定义crd,然后插件通过api-server去操作这些crd实现的(当然本质上还是存在 etcd 中)
  2. 节点内 pod 间通信
  3. 不同节点的 pod 间通信

——a、实现完 IPAM,约等于我们实现 CNI 插件的第一步“实现 IP 地址管理”的功能,怎么设计 IPAM(ip address manager)呢?创建 etcd 的客户端(调用 etcd 官方提供的 etcd 包),必须得将 k8s 的 ca 根证书,以及 etcd 客户端需要使用的证书和私钥作为配置项传给 etcd 包,不然无法连接到 https 的 etcd 集群中。实现 IPAM 需要在 etcd 中创建一个 IP 地址池,设置这个 pool 的目的就是每当我们有一个节点加入到集群中,一旦该节点上的 kubelet 调用了咱们的插件,也就会触发在插件中使用到的 ipam 服务,ipam 服务在节点上初始化的时候,就会去从这个 pool 中拿出一个未使用的网段作为自己这台节点的网段。除了需要在 etcd 中设置一个 pool 用来保存网段之外,我还在 etcd 中为每个节点设置了一个用来记录“当前节点已使用的 ip”的 key。

——b、实现同一台节点上,互相隔离的两个 pod 之间通信,一般采用的办法就是 veth pair + bridge。(​K8s 网络之 从 0 实现一个 CNI 网络插件 - 知乎 (zhihu.com)​

——c、要实现让不同节点的设备互通方法有很多,不过总的归类来讲,大致有四种:

  1. SDN 网络
  2. 走主机的静态路由,这种方式相对简单
  3. overlay 网络,也叫隧道网络
  4. 动态路由网络

类似第二种方式,在 flannel 里叫 gw-host,第三种方式可以使用 vxlan 或者其他隧道网络技术,第四种是 calico 主要采用的方式,使用 BGP 协议在多台主机上做动态路由设置。主机路由的原理,其实也非常简单:

  • etcd 中获取到除了本节点以外的所有节点的 pod 所在网段地址
  • 从 etcd 中获取除了本节点以外的所有节点的 ip 地址
  • 将这些网段地址设置在本机的路由表中,其他节点网段地址为 dst,其他节点的 ip 为 gw,或者叫下一跳(via)
  • 设置主机网卡 iptables 为允许做 ip forward

——实现 cni 插件,主要是通过 veth-pair & bridge 实现的同节点之间通信,然后以 etcd 为依赖实现了一个 ipam 用来管理 ip 地址们,做到了简单的 host-gw 这种动态路由的方式,从而实现了不同节点之间的通信。新的 "ebpf & vxlan" 模式(参考 cilium 的原理实现一个基于 ebpf 和 vxlan 的 cni 网络插件),做到同节点上依赖 ebpf 通信,不同节点不同网段依赖 ebpf & vxlan 来实现网络通信。


Calico 是 tigera 公司开源的一个 k8s 网络的一整套解决方案。

1、Calico 的主要网络模式有两种,一种是 tunnel 模式,一种是路由模式。其中 tunnel 模式又同时支持 vxlan 和 ipip。(calico 现在也已经支持了 ebpf 模式

2、知识点

  • BGP和IPIP不是二选一的关系,类似via 192.168.31.218等其它host的路由信息需要通过BGP(bird在每台主机上开启BGP协议的客户端进程,bird创建出口 ifacetunl0 设备的规则;Feilx监听etcd)来获取。

# route -n
Destination Gateway Genmask Flags Metric Ref Use Iface
172.17.54.128 192.168.31.218 255.255.255.192 UG 0 0 0 tunl0
[root@k8s-node01 ~]# ip r
172.17.0.0/16 via 192.168.31.219 dev tunl0 proto bird onlink
172.17.54.128/26 via 192.168.31.218 dev tunl0 proto bird onlink //192.168.31.218node的tunl0@NONE172.17.54.128
blackhole 172.17.125.0/26 proto bird //自己的话blackhole

  • 实验室ipip Mode: Always模式。另有ipip Mode:cross-subnet模式,如果只对跨越子网边界的流量进行封装,需要使用跨子网的IP-in-IP方式,假设两个node 都在同一个网段,就会不封装,直接走网络bgp模式;如果不在同一个网段,就会使用IP-in-IP模式。——If your network fabric performs source/destination address checks(二所检测源地址) and drops traffic when those addresses are not recognized, it may be necessary to enable IP-in-IP encapsulation of the inter-workload traffic.
  • 在大型网络规模中,如果仅仅使用BGP client形成mesh全网互联的方案就会导致规模限制,50 台node以上calico建议使用RR(Router Reflector)也就是路由反射器。
  • cali的mac地址并不会在实际的链路层报文中使用,因此calico将所有cali网络设备的mac地址都设置为全e
  • 创建一条黑洞路由(route 10.244.2.0/24 blackhole)的作用, 和 ip route add blackhole 效果一样,走这条路由规则的数据包都会被丢弃(其实可以通过抓包 lo 网卡看到),Calico 中需要黑洞是为了当同一台节点中的 pod 互相访问的时候,不让数据流出到其他节点
  • “/etc/calico/confd/config/bird.cfg”是 Calico 运行 BIRD 最主要的配置文件,这个文件在容器里:

# 这个 krt_tunnel 其实在原始的 BIRD 的 c 代码中是没有的
# 这个变量是 Calico 自己加的, 表示设置路由表是的 iface 要走 tunl0
filter calico_kernel_programming {
if ( net ~ 10.244.0.0/16 ) then {
krt_tunnel = "tunl0";
accept;
}
accept;
}

——Calico 自己 fork 了一份儿 BIRD 的代码,给改成了一个只支持 IPIP 的轻量级 BIRD。整体流程上,其实就是做了大概如下几件事儿:

  • 从 IPAM 中获取一个还没有使用的 ip 地址作为本 pod 的 ip
  • 创建一对儿 veth pair,一边放在 pod 的 netns 中,一边留在 host 上
  • 给 pod 的 netns 设置交换路由
  • 并把 pod 留在 host 的那半拉 veth 开启 proxy_arp 以及 forwarding
  • 然后创建 ipip tunnel 设备
  • 给这个 ipip tunnel 设备一个 ip 地址
  • 开启 ipip tunnel 设备的 forwarding
  • 创建 BIRD 配置文件。注意这里因为每个节点的 ip 之类的配置信息都不一样,所以这里需要通过 template 的方式动态生成模板的字符串
  • 启动从 calico bird 那编译好的 bird 程序,注意这个程序你得想办法让它以独立子进程的方式执行哦
  • 把 pod ip 返回给外边,也就是返回给 kubelet
  • flannel是利用的静态路由表配置或者 vxlan 实现的网络通信。calico是通过 BGP 协议实现了动态路由。

3、不同网络模式是怎么获取信息(每个host是如何获取其他host的路由信息),创建iptables规则

  • host-gw 模式是每次启动一个 pod 时都去 etcd 中拉一下其他节点的 nodeip 以及 podip 的对应信息;
  • ebpf 是通过 watch etcd,当有 pod 被创建时 etcd 自动同步给每个节点;
  • BGP 协议就有点类似于 watch etcd 的那种方式,本质上都是在每个节点上启动一条进程,这条进程会监听本机的路由表以及网络设备变化,同时还能将这些变化发送给在同一个 “自治系统” 中的其他 BGP 客户端。
  • Feilx 是 Calico 中和BIRD程序同步运行的另外一条进程,主要作用就是监听 etcd,当节点上要创建 pod 时,ipam 会分配 ip,然后这个 ip 会写入到 etcd,此时这个 Feilx 就能通过监听 etcd 来感知到被分配了哪些 ip,然后把这些 ip 写入到本机的路由表,此时 BIRD 进程就能监听到路由表发生了变化,然后把新的路由规则根据配置给宣告出去。
  • Feilx 进程除了创建路由表的条目之外,还会创建很多 iptables 规则,通过这些 iptables 规则的一些策略来让 Calico 的网络变得更健壮。

4、BIRD 是一个实现了多种动态路由协议(比如 BGP、OSPF 等),可以运行在 Linux/Unix 操作系统的程序。决定 “哪些本机的路由可以发给其他节点”,“本机可以接收哪些路由信息”,“本机可以和哪些节点形成自治系统”

  • BIRD 在内存中维护了一个自己的路由表,这张路由表和 Linux 中的路由表(FIB 表)不一样。BIRD 的这张表中的内容包含 BIRD 自己从 Linux 的 FIB 路由表中收集到的本机的路由信息,以及从其他 BIRD 客户端接收到的其他节点的路由信息BIRD 会根据一些策略算法,把其他节点发过来的路由信息写入到 Linux 本机的路由表,也就是 FIB 表中,以此来让当前节点感知到其他节点上的 ip 变化。
  • Linux路由表被bird配置了新的路由规则了,就像 calico 这样,会有一些出口 iface 是 tunl0 设备的规则
  •  calico 自己实现了一个 bird,这个 bird 可以在每台主机上开启 BGP 协议的客户端进程,用来做路由宣告。
  • 在运行 BGP 的客户端进程之前,需要指定一个 AS 号,一共有 2^32 次方个,这些自治网络的 AS 号需要由一个叫 IANA 的国际组织来颁发的。从 64512 到 65534 中间的一千多个 AS 号是可以给自己玩着用的。其中 64512 这个号码就是 calico 默认的 BGP 网络的 AS 号。

# 表示当前节点的 ip
router id 192.168.64.14;
# 创建自己的一条邻居
# neighbor 是其他节点的 ip
# source address 是自己的 ip
protocol bgp Mesh_192_168_64_16 from bgp_template {
neighbor 192.168.64.16 as 64512;
source address 192.168.64.14; # The local address we use for the TCP connection
}

5、calico 会通过 deamonset 在每台节点上都运行一个 “calico-node”。

  • 使用 pstree 查看这个容器进程可以发现首先启动了 pause 进程,然后就是通过 “bird6” 以及 “bird” 命令启动了 BIRD 进程,同时还运行了 felix 进程;
  • calico-node容器是一个hostNetwork类型的网络,也就是说它容器里操作网络就相当于操作宿主机的网络。这也是容器中的 BIRD 和 Feilx 进程能修改 host 路由信息的缘故。