networkpolicy的实践——felix

fzu_huang

Networkpolicy的含义与现状

networkpolicy是k8s在很早就提出的一个抽象概念。它用一个对象来描述一类pod的网络出入站规则。关于networkpolicy的语义可以参考我之前的文章

networkpolicy的作用对象是pod,作用效果包括出站、入站,作用效果拓扑包括IP段、namespace、pod、端口、协议。

与以往IaaS服务场景下,针对虚拟机、网卡对象的安全组规则不同,networkpolicy是k8s原语。因此,在k8s场景下,进行网络安全规则的规划时,用networkpolicy能做到更加的灵活和自动化。举个例子:

有一套工作负载A是做类似数据库代理一类的工作,它只允许代理服务B访问,不允许其他业务访问。

  • 在k8s场景下,如果不使用networkpolicy,我们需要规划好A类pod的部署节点,配置相应的ACL规则,将B类pod的IP予以放行,一旦A/B类pod做了扩缩容,可能要在重新配置一份甚至多份ACL规则。
  • 在k8s场景下,我们会给A和B类分别配置label,创建好networkpolicy后限制A只放行B类pod,每当A或B扩缩容时,无需做任何额外操作。

业界的networkpolicy实现

当前社区对于k8s的networkpolicy的实现,不外乎三种方案:

方案依赖案例支持的CNI
基于iptables+ipset实现规则容器流量需要经过宿主机的协议栈calico felix,kube-routercalico、flannel
基于ovs流表实现规则使用openvswitchopenshift-sdnopenshift-sdn
基于ebpf hook实现规则需要较高版本内核ciliumcilium、flannel

从上面的表格可以看出:

  1. 基于ovs流表实现的方案,典型的就是openshift-sdn,ovs table的设计,其中有一个专门的table(tableid=21)就是用来实现networkpolicy的规则。 该方案是直接内建于openshift-sdn项目,基本无法移植。 而openshift-sdn虽然代码开源,但设计上、代码逻辑上与openshift平台耦合还是比较紧密的。比如说:

    1. 公开的openshift-sdn部署方案需要依赖openshift-network-operator
    2. openshift-sdn代码中硬编码了要访问的容器运行时为crio,不支持dockershim
  2. cilium是最先使用ebpf技术实现网络数据面的CNI,它力图实现大而全的容器网络,封装、加密、全面、高性能等特点应有尽有,它对于networkpolicy的支持也已经十分完善。但ebpf hook的实现方式,依赖较高的内核版本,且在数据面排障时比较吃力。ebpf技术对于网络性能的提升很大,未来势必会越来越流行,所以值得关注。
  3. 基于iptables+ipset技术实现的方案,其实在几年前就比较成熟了calico-felix、romana、kube-router等开源的网络方案都是基于此实现了支持networkpolicy。其中,felix是calico网络方案中的一个组件,官方支持在calico中enable networkpolicy,且能够与flannel配合使用。阿里云的terway便是直接套用felix实现了对networkpolicy的支持(最近还套用了cilium)。这套方案要求容器流量要进过宿主机协议栈,否则包就不会进入内核的netfilter模块,iptables规则就无法生效。

业界各种k8s集群产品对于networkpolicy的支持:

产品支持实现
AKS(Azure)Azure CNI使用自研networkpolicy方案
自定义路由CNI使用集成calico方案
自研方案也是基于iptables+ipset实现的
EKS(AWS)直接使用calicocalico天然支持
TKE(腾讯云)直接使用kube-router对kube-router做兼容性改造
ACK(阿里云)veth虚拟网卡使用calico
ipvlan虚拟网卡使用cilium
对calico的felix(v3.5.8)或cilium(v1.8.1)做兼容性改造

目标

基于上述现状,我们希望基于现有的开源实现方案,进行兼容性调研或改造,适配我们自研的网络方案。

因为这些网络方案都满足felix的要求,同时felix有较为活跃的社区和较多的适配案例,因此我们决定基于felix,实现一套即插即用的networkpolicy addon。本文接下来将会着重介绍该方案的实现。

calico/felix的设计实现

架构

calico在部署架构上做了多次演进,我们以最新版本v3.17.1为准。calico的完整架构包括了若干组件:

  • calico/kube-controllers: calico控制器,用于监听一些k8s资源的变更,从而进行相应的calico资源的变更。例如根据networkpolicy对象的变更,变更相应的calicopolicy对象
  • pod2daemon: 一个initcontainer,用于构建一个Unix Domain Socket,来让Felix程序与Dikastes(calico中支持istio的一种sidecar,详见calico的istio集成)进行加密通信.
  • cni plugin / ipam plugin: 标准的CNI插件,用于配置/解除网络;分配/回收网络配置
  • calico-node calico-node其实是一个数据面工具总成,包括了:

    • felix: 管理节点上的容器网卡、路由、ACL规则;并上报节点状态
    • bird/bird6: 用来建立bgp连接, 并根据felix配置的路由,在不同节点间分发
    • confd: 根据当前集群数据生成本地brid的配置
  • calicoctl: calico的CLI工具。
  • datastore plugin: 即calico的数据库,可以是独立的etcd,也可以以crd方式记录于所在集群的k8s中
  • typha: 类似于数据库代理,可以尽量少避免有大量的连接建立到apiserver。适用于超过100个node的集群。

官网给出了calico整体的组件架构图:

原理

在网络连通性(Networking)方面:calico的数据面是非常简单的三层路由转发。路由的学习和分发由bgp协议完成。如果k8s的下层是VPC之类的三层网络环境,则需要进行overlay,calico支持ipip封装实现overlay。

在网络安全性方面:calico考虑到其Networking是依赖宿主机协议栈进行路由转发实现的,因此可以基于iptables+ipset进行流量标记、地址集规划、流量处理(放行或DROP),并且基于这些操作可以实现:

  1. networkpolicy的抽象概念
  2. calico自定义的networkpolicy,为了在openstack场景下应用而设计
  3. calico自定义的profile,已废弃。

这里所有的iptables规则都作用在:

  1. pod在宿主机namespace中的veth网卡(calico中将之称为workload)
  2. 宿主机nodeIP所在网卡(calico中将之称为host-endpoint,实际上这部分规则不属于k8s的networkpolicy范畴)。

主要包括如下几类规则(注意这些规则都针对宿主机上的网卡,如node网卡,pod的host-veth):

  • iptables的INPUT链规则中,会先跳入cali-INPUT 链,在cali-INPUT链中,会判断和处理两种方向的流量:

    • pod访问node(cali-wl-to-host)实际上这个链中只走了cali-from-wl-dispatch链,如果是应用在openstack中,该链还会允许访问metaserver;如果使用ipv6,该链中还会允许发出icmpv6的一系列包
    • 来自node的流量(cali-from-host-endpoint)
  • iptables的OUTPUT链中,会首先跳入cali-OUTPUT链,在cali-OUTPUT链中,主要会处理:

    • 不作任何处理。
    • 特殊情况下处理node访问node的流量(cali-to-host-endpoint
  • iptables的FORWARD链中,会首先跳入cali-FORWARD链,在cali-FORWARD链中会处理如下几种流量:

    • 来自node转发的流量cali-from-hep-forward
    • 从pod中发出的流量cali-from-wl-dispatch
    • 到达pod的流量cali-to-wl-dispatch
    • 到达node的转发流量cali-to-hep-forward
    • 纯粹的IP段到IP段的转发流量cali-cidr-block

k8s的networkpolicy只需要关注上述流量中与pod相关的流量,因此只需要关心:

  • cali-from-wl-dispatch
  • cali-to-wl-dispatch

这两个链的规则,对应到pod的egress和ingress networkpolicy。

1. 除了nat表,在raw和mangle表中还有对calico关注的网卡上的收发包进行初始标记的规则,和最终的判断规则。
2. 在https://github.com/projectcalico/felix/blob/master/rules/static.go中可以看到完整的静态iptables表项的设计

接着,iptables规则中还会在cali-from-wl-dispatchcali-to-wl-dispatch两个链中根据收包/发包的网卡判断这是哪个pod,走到该pod的egress或ingress链中。每个pod的链中则又设置了对应networkpolicy实例规则的链,以此递归调用。

这样,pod的流量经过INPUT/OUTPUT/FORWARD等链后,递归地走了多个链,每个链都会Drop或者Return,如果把链表走一遍下来一直Return,会Return到INPUT/OUTPUT/FORWARD, 然后执行ACCEPT,也就是说这个流量满足了某个networkpolicy的规则限制。如果过程中被Drop了,就表示受某些规则限制,这个链路不通。

我们通过一个简单的例子来描述iptables这块的链路顺序。

felix实现networkpolicy的案例1

假设有如下一个networkpolicy:

  spec:
    egress:
    - {}
    ingress:
    - from:
      - podSelector:
          matchLabels:
            hyapp: client1
    - from:
      - ipBlock:
          cidr: 10.16.2.0/24
          except:
          - 10.16.2.122/32
      ports:
      - port: 3456
        protocol: TCP
    podSelector:
      matchLabels:
        hyapp: server
  • 他作用于有hyapp=server的label的pod
  • 这类pod出方向不限制
  • 这类pod的入站规则中只允许如下几种流量:

    • 来自于有hyapp=client1的label的pod
    • 10.16.2.0/24网段中除了10.16.2.122/32以外的IP可以访问该类pod的3456 TCP端口。

我们使用iptables -Liptables-save 命令来分析机器上的iptables规则。

因为是入站规则,所以我们可以观察iptables表中的cali-to-wl-dispatch链。另外,该networkpolicy的作用pod只有一个,它的host侧网卡是veth-13dd25c5cb。我们可以看到如下的几条规则:

Chain cali-to-wl-dispatch (1 references)
target     prot opt source               destination
cali-to-wl-dispatch-0  all  --  anywhere             anywhere            [goto]  /* cali:Ok_j0t6AwtLyoFYU */
cali-tw-veth-13dd25c5cb  all  --  anywhere             anywhere            [goto]  /* cali:909gC5dwdBI3E96S */
DROP       all  --  anywhere             anywhere             /* cali:4M4uUxEEGrRKj1PR */ /* Unknown interface */

注意,这里有一个cali-to-wl-dispatch-0的链,是用来做前缀映射的, 该链的规则下包含所有cali-tw-veth-0 这个前缀的链:

Chain cali-to-wl-dispatch-0 (1 references)
target     prot opt source               destination
cali-tw-veth-086099497f  all  --  anywhere             anywhere            [goto]  /* cali:Vt4xxuTYlCRFq62M */
cali-tw-veth-0ddbc02656  all  --  anywhere             anywhere            [goto]  /* cali:7FDgBEq4y7PN7kMf */
DROP       all  --  anywhere             anywhere             /* cali:up42FFMQCctN8FcW */ /* Unknown interface */

这是felix设计上用于减少iptables规则遍历次数的一个优化手段。

我们通过iptables-save |grep cali-to-wl-dispatch命令,可以发现如下的规则:

cali-to-wl-dispatch -o veth-13dd25c5cb -m comment --comment "cali:909gC5dwdBI3E96S" -g cali-tw-veth-13dd25c5cb

意思就是:在cali-to-wl-dispatch链中,根据pod在host侧网卡的名字,会执行cali-tw-veth-13dd25c5cb链, 我们再看这条链:

   Chain cali-tw-veth-13dd25c5cb (1 references)
   target     prot opt source               destination
1  ACCEPT     all  --  anywhere             anywhere             /* cali:RvljGbJwZ8z9q-Ee */ ctstate RELATED,ESTABLISHED
2  DROP       all  --  anywhere             anywhere             /* cali:krH_zVU1BetG5Q5_ */ ctstate INVALID
3  MARK       all  --  anywhere             anywhere             /* cali:Zr20J0-I__oX_Y2w */ MARK and 0xfffeffff
4  MARK       all  --  anywhere             anywhere             /* cali:lxQlOdcUUS4hyf-h */ /* Start of policies */ MARK and 0xfffdffff
5  cali-pi-_QW8Cu1Tr3dYs2pTUY0-  all  --  anywhere             anywhere             /* cali:d2UTZGk8zG6ol0ME */ mark match 0x0/0x20000
6  RETURN     all  --  anywhere             anywhere             /* cali:zyuuqgEt28kbSlc_ */ /* Return if policy accepted */ mark match 0x10000/0x10000
7  DROP       all  --  anywhere             anywhere             /* cali:DTh9dO0o6NsmIQSx */ /* Drop if no policies passed packet */ mark match 0x0/0x20000
8  cali-pri-kns.default  all  --  anywhere             anywhere             /* cali:krKqEtFijSLu5oTz */
9  RETURN     all  --  anywhere             anywhere             /* cali:dgRtRf38hD2ZVmC7 */ /* Return if profile accepted */ mark match 0x10000/0x10000
10 cali-pri-ksa.default.default  all  --  anywhere             anywhere             /* cali:NxmrZYbhCNLKgL6O */
11 RETURN     all  --  anywhere             anywhere             /* cali:zDbjbrN6JPMZx9S1 */ /* Return if profile accepted */ mark match 0x10000/0x10000
12 DROP       all  --  anywhere             anywhere             /* cali:d-mHGbHkL0VRl6I6 */ /* Drop if no profiles matched */
  • 第1、2条:如果ct表中能检索到该连接的状态,我们直接根据状态来确定这个流量的处理方式,这样可以省略很大一部分工作。
  • 第3条:先对包进行标记(将第17位置0),在本链的规则执行完毕后,会判断标记是否match(判断第17位是否有被置1),不匹配(没有被置1)就DROP;
  • 第4条:如果该网卡对应的pod有相关的networkpolicy,要再打一次mark,与之前的mark做与计算后目前mark应该是0xfffcffff(17、18位为0);
  • 第5条:如果包mark match 0x0/0x20000(第18位为0), 执行cali-pi-_QW8Cu1Tr3dYs2pTUY0-链进入networkpolicy的判断。
  • 第6、7条:如果networkpolicy检查通过,会对包进行mark修改, 所以检查是否mark match 0x10000/0x10000, 匹配说明通过,直接RETURN,不再检查其他的规则;如果mark没有修改,与原先一致,视为没有任何一个networkpolicy允许该包通过,直接DROP
  • 第8、9、10、11条:当没有任何相关的networkpolicy时(即第4~7条不存在)才会被执行,执行calico的profile策略,分成namespace维度和serviceaccount维度,如果在这两个策略里没有对包的mark做任何修改,就表示通过。这两个策略是calico的概念,且为了不与networkpolicy混淆,已经被弃用了。因此此处都是空的。
  • 第12条:如果包没有进入 上述两个profile链,DROP。

接着看networkpolicy的链cali-pi-_QW8Cu1Tr3dYs2pTUY0-,只要在这个链里执行Return前有将包打上mark使其match 0x10000/0x10000,就表示匹配了某个networkpolicy规则,包允许放行:

Chain cali-pi-_QW8Cu1Tr3dYs2pTUY0- (1 references)
target     prot opt source               destination
MARK       all  --  anywhere             anywhere             /* cali:fdm8p72wShIcZesY */ match-set cali40s:9WLohU2k-3hMTr5j-HlIcA0 src MARK or 0x10000
RETURN     all  --  anywhere             anywhere             /* cali:63L9N_r1RGeYN8er */ mark match 0x10000/0x10000
MARK       all  --  anywhere             anywhere             /* cali:xLfB_tIU4esDK000 */ MARK xset 0x40000/0xc0000
MARK       all  --  10.16.2.122          anywhere             /* cali:lUSV425ikXY6zWDE */ MARK and 0xfffbffff
MARK       tcp  --  10.16.2.0/24         anywhere             /* cali:8-qnPNq_KdC2jrNT */ multiport dports 3456 mark match 0x40000/0x40000 MARK or 0x10000
RETURN     all  --  anywhere             anywhere             /* cali:dr-rzJrx0I6Vqfkl */ mark match 0x10000/0x10000
  • 第1、2条:如果src ip match ipset:cali40s:9WLohU2k-3hMTr5j-HlIcA0 ,将包 mark or 0x10000, 并检查是否match,match就RETUR。 我们可以在机器上执行ipset list cali40s:9WLohU2k-3hMTr5j-HlIcA0 , 可以看到这个ipset里包含的就是networkpolicy中指明的、带有hyapp=client1这个label的两个pod的ip。
  • 第3、4、5、6条则是针对networkpolicy中的第二部分规则,先对包设置正向标记,然后将要隔离的src IP/IP段进行判断并做反向标记,接着判断src段是否在准入范围,如果在,并且目的端口匹配,并且标记为正向,就再对包进行MARK or 0x10000 , 这样,最终判断match了就会Return。
  • 实际上我们可以看到,这里就算不match,这个链执行完了也还是会RETURN的,所以这个链执行的结果是通过mark返回给上一级的,这就是为什么调用该链的上一级,会在调用完毕后要判断mark并确认是否ACCEPT。

至此,一个完整的networkpolicy的实现链路就完成了。

felix实现networkpolicy的案例2

思考一个问题:

假设我们对pod的出入站规则都做了限制,pod是否就完全被隔离了呢?如果pod被完全隔离,那么pod中设计的健康检查规则是否就不生效了?这样pod是否就永远无法ready?

felix对networkpolicy的实现主要集中在iptables的FORWARD链中,而k8s对pod的健康检查是由node访问pod。基于不同的网络方案,有几种不同的链路:

  • flannel。node上基于路由直接找到flannel网桥(默认是cbr0),网桥内部二层转发到veth。最后包是从pod的host-veth发出,在容器内的veth收到包。
  • calico。node上基于scope link路由直接找到pod的host-veth,最后包是从pod的host-veth发出,在容器内的veth收到包。

可见,包会走host-veth的OUTPUT链,整个过程中没有经过host-veth的FORWARD链。上文我们提到,在cali-OUTPUT中没有任何特殊规则,因此包可以顺利送达容器。

容器回包时,host-veth是要收包的,基于回包的目的IP的不同,可能走PREROUTING->INPUT, 或者PREROUTING->FORWARD。 两种路径不管哪种都会经过cali-from-wl-dispatch链,从而进入对pod的egress规则的判断。

egress规则中我们禁止pod出站的所有规则,那回包应该会被DROP吧?

答案是不会,因为请求包在CT表中构成了记录,回包与请求包是同一个连接的,因此这种关联包会被felix设计的iptables规则放行(在例1中我们有提到入站的networkpolicy链里也有类似的规则)。

Chain cali-fw-veth-877c5ccffd (1 references)
target     prot opt source               destination
ACCEPT     all  --  anywhere             anywhere             /* cali:OEbQGqO7Ne_PHJxs */ ctstate RELATED,ESTABLISHED
DROP       all  --  anywhere             anywhere             /* cali:2VWpV2kgclFmj3-s */ ctstate INVALID

那是否意味着实际上这个networkpolicy规则对于pod和node之间的网络不生效呢?

不尽然。尽管node可以访问pod,但pod内无法直接访问node上的IP,这是因为pod内主动发出的包请求,在host-veth侧直接进入cali-from-wl-dispatch链,此时ctstate是 NEW,而非RELATED或ESTABLISHED。因此要接着走下面的networkpolicy规则。

总结

综上所述。felix对networkpolicy的实现,处理了:

  • node上经过转发后进入pod的流量(即ingress)
  • pod内主动向外发出的流量(即egress)

基于上述的案例,我们可以分析出felix设计的networkpolicy iptables规则的大致流程,参考下图:

  • Tip1:图中的cali-fw-veth***cali-tw-veth***链下,都会首先判断连接的状态,如果ctstate是RELATED或ESTABLISHED,是允许放行的。(关于ctstate的说明可以参考这篇文章

通用的felix插件设计

如果你看了上文calico/felix的设计实现,你就会发现原理其实非常简单,这个设计完全可以应用到任何一个“基于三层路由转发”的网络方案中。但实际应用过程中我们还是遇到了一些问题。

问题1:networkpolicy-only

我们知道,较新版本的felix都是集成到calico-node组件中运行。calico-node默认情况下会完成容器网络和networkpolicy两块工作,如何部署一个只负责实现networkpolicy规则的calico-node呢?

可以参考calico官方提供的canal方案是如何适配flannel的。从canal的部署模板中我们可以基本确认,只要部署好kube-controllers,pod2daemon,calico-node并且通过环境变量控制calico-nodeCALICO_NETWORKING环境变量为"false"(禁止配置容器网络)即可。

另外,AWS的calico部署模板 ,则是只部署calico-nodetypha,有兴趣的同学也可以实践一下。

问题2:网卡名映射

我们尝试在网易轻舟k8s集群(使用网易云VPC作为容器网络)中尝试以这样的方式部署一套calico套件,部署后,我们会发现calico-node的日志里定期报错,提示:找不到cali****的网卡,无法配置iptables规则。 有用过calico的同学应该看得明白,cali是calico方案在宿主机侧生成的网卡名前缀。而我们基于网易云VPC设计的容器网络方案,会以veth-为容器hostveth前缀。如果calico是基于前缀来找到容器网卡的,那么是否有参数可以指定前缀呢?

官方的felix配置文档中提到:可以使用InterfacePrefix参数或FELIX_INTERFACEPREFIX环境变量,决定felix要检索的host侧网卡前缀。一开始看到这个说明令人欣喜万分。但是当我们实际配置了之后,会发现,calico-node还是会报错,提示: 找不到veth-*****的网卡,这个网卡名超过了linux内核的常数限制(15个字符)

我们按照日志里打印的网卡名去找,确实找不到这个网卡,看来必须要搞清楚calico是如何给host侧的网卡进行命名的。

calico为pod的veth命名的规则实现在libcalico-go项目中,存在如下的一个接口

type WorkloadEndpointConverter interface {
    VethNameForWorkload(namespace, podName string) string
    PodToWorkloadEndpoints(pod *kapiv1.Pod) ([]*model.KVPair, error)
}

这个接口用用来实现pod映射到workload的,同时还能根据pod的信息,推导pod的hostveth网卡名是啥,该接口只有一种实现:defaultWorkloadEndpointConverter, 其中VethNameForWorkload的实现如下:

// VethNameForWorkload returns a deterministic veth name
// for the given Kubernetes workload (WEP) name and namespace.
func (wc defaultWorkloadEndpointConverter) VethNameForWorkload(namespace, podname string) string {
    // A SHA1 is always 20 bytes long, and so is sufficient for generating the
    // veth name and mac addr.
    h := sha1.New()
    h.Write([]byte(fmt.Sprintf("%s.%s", namespace, podname)))
    prefix := os.Getenv("FELIX_INTERFACEPREFIX")
    if prefix == "" {
        // Prefix is not set. Default to "cali"
        prefix = "cali"
    } else {
        // Prefix is set - use the first value in the list.
        splits := strings.Split(prefix, ",")
        prefix = splits[0]
    }
    log.WithField("prefix", prefix).Debugf("Using prefix to create a WorkloadEndpoint veth name")
    return fmt.Sprintf("%s%s", prefix, hex.EncodeToString(h.Sum(nil))[:11])
}

可以看到,calico根据pod的namespace和name进行hash,然后根据FELIX_INTERFACEPREFIX环境变量的值决定网卡名前缀,将前缀与hash的前11个字符拼凑起来。 libcalico-go是所有calico组件的lib库,也就是说,不论是calico-cni去创建veth,还是felix去根据pod查找对应的网卡, 都是基于这个逻辑去匹配的。

显然这个代码漏洞很大!calico没有对前缀做长度检查,这里要填充hash的前11位,完全是因为默认的前缀是四个字符的cali

问题非常明确了,要想在自己的网络方案下无痛享受felix,就得自己实现一个WorkloadEndpointConverter接口,并编译出定制化的calico-node镜像。

案例1: canal如何接入

从canal的部署模板中我们就可以看得出来,canal方案中使用的CNI plugin实际上也是calico,只不过calico只负责创建veth对,配置IP和路由等工作,veth的命名交给calico来做,自然就按照calico的官方配置来命名了,实际使用过程中就可以看到,canal方案下容器在宿主机上的veth名称也是cali前缀。

案例2:阿里云terway如何接入

阿里云的ACK使用其自研的terway来作为容器网络方案。terway中支持两种容器网卡虚拟化方案:

  • veth
  • ipvlan L2

veth方案下会使用felix来实现networkpolicy,而ipvlan下则使用cilium。我们此处主要关注veth方案。

veth方案下felix是如何使用的呢?terway在部署时,直接基于社区v3.5.8版本的felix代码进行编译(编译前还往代码中加入了一个terway自定义的patch),将编译出来的felix二进制文件丢到terway的docker镜像中, daemonset里启动三个terway镜像容器,分别用于安装cni插件;运行agent;运行felix。

terway是如何实现兼容felix的呢?上文提到的网卡名的问题,它如何解决呢?

通过阅读terway的源码 ,我们发现terway做得比较暴力——直接复用了calico代码中的网卡命名方式,对host侧的veth进行命名,网卡前缀为硬编码的cali

自给自足——基于calico源码改造设计通用的networkpolicy

在我们的自研网络方案中,host侧网卡命名是以某个前缀加上pod的sandbox容器id来命名的(原因见下文)。因此我们即便把前缀改成cali或者其他长度4以内的字符串,felix也无法基于calico的那套逻辑找到网卡。

因此我们改写了该接口。实现了一个sandboxWorkloadEndpointConverter, 将VethNameForWorkload做了另一种实现:

  1. 根据felix参数感知自定义的网卡名前缀,这里为了避免prefix太长,导致网卡名冲突,对prefix长度进行限制,建议不超过5个字符,至少给后缀保留10个字符(我们曾经在线上环境出现过同一个节点的两个podsandbox容器id前9位完全相同的情况)
  2. 根据pod信息获取到他对应的sandbox容器ID,取其15-len(prefix)位作为后缀。
  3. 通过前缀与sandboxID后缀构成workload的网卡名。

未来我们会尝试对这部分代码做更通用化的改造,支持多种前缀,并支持自动选用网卡命名方法。

为什么我们要以podsandbox容器id来命名网卡?

因为实际使用过程中我们发现,kubelet对于sandbox容器的处理并不一定是有序的,可能出现如下场景:

  1. 为poda创建出sandbox1, 调用CNI ADD失败,但veth已经创建;
  2. 为poda创建出sandbox2, 调用 CNI ADD成功,直接使用了上一次创建的veth;
  3. 删除此前已经失败的sandbox1,调用 CNI DEL。 将第2步创建的veth删除,导致poda的网络异常

因此,如果kubelet调用CNI是以sandbox为粒度,那么我们创建的资源就理应也以sandbox为粒度。

编译与构建

目前我们将通用的felix基于calico/node的v3.17分支构建, 并将它引用的libcalico-gofork至:163yun/libcalico-go,并建立分支tag:v1.7.2-nks.1

这样,我们可以直接拉取社区代码,进行编译:

cd $GOPATH/src
mkdir -p github.com/projectcalico
cd github.com/projectcalico
git clone https://github.com/projectcalico/node
cd node

# 修改依赖包,改为引用我们修改过后的libcalico-go
go mod edit -replace=github.com/projectcalico/libcalico-go=github.com/163yun/libcalico-go@v1.7.2-nks.1
# 编译出calico/node的docker  image
make calico/node

docker tag calico/node:latest-amd64 $EXPECTED_IMAGE_PATH:$EXPECTED_IMAGE_TAG

编译构建过程中可能出现一些网络原因导致编译阻塞:

  • 编译过程中会在容器里进行go build,为了方便执行go module,所以建议在Makefile、以及执行make时下载的临时版本Makefile.common.v***文件中的部分位置注入环境变量:GOPROXY=https://goproxy.cn
  • 编译完毕后构建docker image时,会在基础镜像中下载安装多个依赖工具,可能出现yum源无法解析等问题,建议在Makefile文件中调用docker build的语句里追加参数--network=host

测试方法

在一套现成的k8s集群中部署felix套件(参考上图)。

我们使用sonobuoy工具进行测试工作,该工具也可以从来进行k8s集群的conformance认证。下载该工具的二进制文件,然后执行:

sonobuoy  run --e2e-focus="\[Feature:NetworkPolicy\]" --e2e-skip="" --image-pull-policy IfNotPresent 

即可在当前集群里进行networkpolicy相关的e2e测试(并且测试过程创建的pod的imagePullPolicy都是IfNotPresent)。

执行命令后可以查看集群中的pods,会看到sonobuoy的pod以及它创建出来的一些e2e相关的pod,如果有pod阻塞于ImagePullBackoff,可以尝试在pod所在节点上拉取备用镜像并修改成所需镜像:

docker pull hub.c.163.com/combk8s/conformance:v1.19.3
docker tag hub.c.163.com/combk8s/conformance:v1.19.3 k8s.gcr.io/conformance:v1.19.3
docker pull hub.c.163.com/combk8s/e2e-test-images/agnhost:2.20
docker tag hub.c.163.com/combk8s/e2e-test-images/agnhost:2.20 k8s.gcr.io/e2e-test-images/agnhost:2.20

整个networkpolicy的e2e测试用例有29个,整体测试完毕耗时约1~1.5h。

测试完毕后,理论上所有e2e前缀的pod都会被删除,此时执行sonobuoy retrieve命令, 会在当前目录生成一个tar.gz文件,解压该文件,并读取plugins/e2e/results/global/e2e.log , 就可以看到整个e2e测试的执行结果。 也可以通过plugins/e2e/sonobuoy_results.yaml文件查看,但这个文件内容包括了未执行的用例,可读性可能不太好。

简要的e2e测试结果如下:

root@pubt2-nks-for-dev6:/home/hzhuangyang1/plugins/e2e/results/global# tail -n 10 e2e.log

JUnit report was created: /tmp/results/junit_01.xml
{"msg":"Test Suite completed","total":29,"completed":29,"skipped":5204,"failed":0}

Ran 29 of 5233 Specs in 4847.335 seconds
SUCCESS! -- 29 Passed | 0 Failed | 0 Pending | 5204 Skipped
PASS

Ginkgo ran 1 suite in 1h20m48.75547671s
Test Suite Passed
阅读 1.5k

kubernetes_docker集群管理
我学到的关于k8s和docker的种种。 仅作学习笔记用,不会发布在别的地方
272 声望
105 粉丝
0 条评论
272 声望
105 粉丝
文章目录
宣传栏