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支持
网易数帆的轻舟容器平台产品,在对外部用户交付中,会交付openshift-sdn方案或calico方案,这些方案都可以原生地支持networkpolicy(下文会介绍)。
而对于集团内部用户大多会交付适合集团内机房网络架构的容器网络方案(vpc或bgp)。由于内部用户大多是由PE规划管理IP白名单,限制某些网络访问,除此之外没有做任何跨业务的网络限制(比如说:离线转码业务与支付业务是互不相干的,但是两种业务的pod彼此网络是可通信的)。因此networkpolicy的需求并不旺盛。
但是,未来随着业务规模的扩大,类似的网络安全策略是必不可少的,因此我们正在逐步将networkpolicy 推广到内部的一些业务场景中。那么,我们要如何实现networkpolicy的功能呢?
业界的networkpolicy实现
当前社区对于k8s的networkpolicy的实现,不外乎三种方案:
方案 | 依赖 | 案例 | 支持的CNI |
---|---|---|---|
基于iptables+ipset实现规则 | 容器流量需要经过宿主机的协议栈 | calico felix,kube-router | calico、flannel |
基于ovs流表实现规则 | 使用openvswitch | openshift-sdn | openshift-sdn |
基于ebpf hook实现规则 | 需要较高版本内核 | cilium | cilium、flannel |
从上面的表格可以看出:
基于ovs流表实现的方案,典型的就是openshift-sdn,此前我们分享过一篇openshift-sdn的详解,介绍了里面对ovs table的设计,其中有一个专门的table(tableid=21)就是用来实现networkpolicy的规则。 该方案是直接内建于openshift-sdn项目,基本无法移植。 而openshift-sdn虽然代码开源,但设计上、代码逻辑上与openshift平台耦合还是比较紧密的。比如说:
- 公开的openshift-sdn部署方案需要依赖openshift-network-operator
- openshift-sdn代码中硬编码了要访问的容器运行时为crio,不支持dockershim
- cilium是最先使用ebpf技术实现网络数据面的CNI,它力图实现大而全的容器网络,封装、加密、全面、高性能等特点应有尽有,它对于networkpolicy的支持也已经十分完善。但ebpf hook的实现方式,依赖较高的内核版本,且在数据面排障时比较吃力。ebpf技术对于网络性能的提升很大,未来势必会越来越流行,所以值得关注。
- 基于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) | 直接使用calico | calico天然支持 |
TKE(腾讯云) | 直接使用kube-router | 对kube-router做兼容性改造 |
ACK(阿里云) | veth虚拟网卡使用calico ipvlan虚拟网卡使用cilium |
对calico的felix(v3.5.8)或cilium(v1.8.1)做兼容性改造 |
目标
基于上述现状,我们希望基于现有的开源实现方案,进行兼容性调研或改造,适配网易数帆的各种网络方案,如:
- netease-vpc
- netease-bgp
- flannel
- ...
因为这些网络方案都满足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),并且基于这些操作可以实现:
- networkpolicy的抽象概念
- calico自定义的networkpolicy,为了在openstack场景下应用而设计
- calico自定义的profile,已废弃。
这里所有的iptables规则都作用在:
- pod在宿主机namespace中的veth网卡(calico中将之称为workload)
- 宿主机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
)
- pod访问node(
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
- 来自node转发的流量
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-dispatch
和cali-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 -L
或iptables-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-node
的CALICO_NETWORKING
环境变量为"false"
(禁止配置容器网络)即可。
另外,AWS的calico部署模板 ,则是只部署calico-node
和typha
,有兴趣的同学也可以实践一下。
问题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
。
案例3:网易云k8s如何接入
网易云的场景中,host侧网卡命名是以某个前缀加上pod的sandbox容器id来命名的(原因见下文)。因此我们即便把前缀改成cali
或者其他长度4以内的字符串,felix也无法基于calico的那套逻辑找到网卡。
因此我们改写了该接口。实现了一个sandboxWorkloadEndpointConverter
, 将VethNameForWorkload
做了另一种实现:
- 根据felix参数感知自定义的网卡名前缀,这里为了避免prefix太长,导致网卡名冲突,对prefix长度进行限制,建议不超过5个字符,至少给后缀保留10个字符(我们曾经在线上环境出现过同一个节点的两个podsandbox容器id前9位完全相同的情况)
- 根据pod信息获取到他对应的sandbox容器ID,取其
15-len(prefix)
位作为后缀。 - 通过前缀与sandboxID后缀构成workload的网卡名。
未来我们会尝试对这部分代码做更通用化的改造,支持多种前缀,并支持自动选用网卡命名方法。
为什么我们要以podsandbox容器id来命名网卡?
因为实际使用过程中我们发现,kubelet对于sandbox容器的处理并不一定是有序的,可能出现如下场景:
- 为poda创建出sandbox1, 调用CNI ADD失败,但veth已经创建;
- 为poda创建出sandbox2, 调用 CNI ADD成功,直接使用了上一次创建的veth;
- 删除此前已经失败的sandbox1,调用 CNI DEL。 将第2步创建的veth删除,导致poda的网络异常。
因此,如果kubelet调用CNI是以sandbox为粒度,那么我们创建的资源就理应也以sandbox为粒度。
编译与构建
目前我们将通用的felix基于calico/node的v3.17分支构建, 并将它引用的libcalico-go
fork到网易云的github organization项目: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/[email protected]
# 编译出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集群,并使用vpc或bgp网络方案,并额外部署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
总结
本文介绍了networkpolicy的特点和优势,并分析了当下主流的networkpolicy实现方案——calico-felix,探索了calico-felix通用化改造的方案和落地。随着calico-felix的引入,用户能同时享受到vpc的易扩展性和networkpolicy的灵活性。
未来我们还将着力引入cilium实现高内核版本下、基于ebpf实现的networkpolicy,并实现networkpolicy拓扑可视化。