CNI学习笔记
Kubernetes没有提供默认可用的容器网络,第三方提供的容器网络,必须满足以下条件:
- 容器之间可以相互通信,且不需要NAT
- 宿主机和容器可以相互通信,且不需要NAT
- 容器看到自己的IP,和其它节点/容器看到的它的IP,是一样的
即集群包含的每一个容器都拥有一个与其他集群中的容器和节点可直接路由的独立IP地址。但是Kubernetes并没有具体实现这样一个网络模型,而是实现了一个开放的容器网络标准CNI。
K8S容器网络,具有两种实现风格:
- Overlay Network,即通用的虚拟化网络模型,不依赖于宿主机底层网络架构,可以适应任何的应用场景,方便快速体验。但是性能较差,因为在原有网络的基础上叠加了一层Overlay网络,封包解包或者NAT对网络性能都是有一定损耗的
- Underlay Network,即基于宿主机物理网络环境的模型,容器与现有网络可以直接互通,不需要经过封包解包或是NAT,其性能最好。但是其普适性较差,且受宿主机网络架构的制约,比如MAC地址可能不够用
容器网络接口(Container Network Interface,CNI)是CNCF项目之一,它关注容器的网络连接,负责在容器启动时创建网络资源,在容器删除时清理为其创建的网络资源。由于网络和运行环境关系很密切,因此很必要进行插件化,这是CNI项目创建的缘由。
CNI项目包含两大部分:
- CNI规范文档:
- libcni,Go语言的CNI接口,供运行时调用,转调具体CNI插件
- skel,Go语言的CNI插件骨架
- https://github.com/containernetworking/cni
- 一系列参考实现和样例插件:
- 接口插件:ptp、bridge、macvlan……
- Chained插件:portmap、bandwidth、tuning
- https://github.com/containernetworking/plugins
具有以下特点:
- 供应商中立,不仅仅是为K8S设计。还可以被Mesos、CloudFoundry、podman、CRI-O使用
- 为网络操作定义了基本的执行流、配置格式
- 简单、向后兼容
JSON格式,对于任何操作,都从标准输入喂给插件。可以包含标准的配置项、插件特有的配置项,示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "name": "mynet", "type": "bridge", "bridge": "mynet0", "isDefaultGateway": true, "forceAddress": false, "ipMasq": true, "hairpinMode": true, "ipam": { "type": "host-local", "subnet": "10.10.0.0/16" } } |
- CNI插件是可执行文件,支持ADD / DEL / CHECK / VERSION几个命令
- 当期望进行网络配置操作时,由容器运行时调用并产生CNI插件进程
- JSON配置、容器相关的数据,通过stdin传递给CNI插件
- CNI插件通过stdout报告处理结果
在未来,CNI可能会支持:
- 动态更新现有网络配置
- 网络带宽和防火墙策略的动态策略支持
当前CNI版本是0.4.0,规范的版本和CNI库的版本、插件样例/参考实现的版本无关。该版本主要增加了CHECK命令。
CNI规范来自rkt网络提案,它规定了Linux下应用容器网络的插件化解决方案。在此规范中,术语:
- 容器(container)可以认为是Linux网络命名空间的同义词,其关联的单元取决于容器运行时,对于K8S来说是Pod,对于Docker来说是一个裸容器
- 网络(network)是一组可被唯一寻址的实体,它们能够相互通信。这些实体可以是上一条提出的容器、一个主机、某些网络设备(例如路由器)。容器可以被添加到1-N个网络,或者从中删除
CNI规范规定的是运行时和插件之间的接口。
- 在调用任何插件之前,容器运行时必须先为容器创建网络命名空间
- 运行时必须确定,容器属于哪些网络,对于这些网络,分别应当调用什么插件
- 配置文件格式为JSON,包含name、type等必须字段。每次插件调用,参数可以不一样,为此目的,可选的 args字段必须包含变化的信息
- 为了将容器添加到每个网络,运行时必须逐个的为每个网络调用插件
- 在容器销毁时,运行时必须以相反顺序调用插件,将容器从网络断开
- 运行时可以并行的为多个容器调用插件,但是对于单个容器,必须串行调用
- 容器必须有唯一性的标识ContainerID,如果需要存储状态,插件必须以网络名、CNI_CONTAINERID、CNI_IFNAME(容器内接口名)作为联合主键
- 对于任一个上述联合主键,ADD不得被运行时调用两次。这隐含表示:一个容器不得被加入同一网络两次,除非它具有两个网络接口
插件必须实现为可执行文件,rkt/K8S等容器管理系统将通过命令行来调用它们。
CNI插件负责把一个网络接口(例如VETH对的一端)插入到容器的网络命名空间,并在宿主机上执行任何必要的操作(例如将VETH对的另外一端接到网桥)。CNI插件还必须给接口分配IP地址,并且通过调用适当的IPAM插件,创建和IPAM一致的路由规则。
该接口负责将容器添加到网络中。
参数:
- Container ID,容器唯一标识符文本,由运行时分配,不得为空。以数字/字母开头,可以包含 _.-字符
- Network namespace path,网络命名空间的路径,形式 /proc/PID/ns/net,或者指向该文件的绑定挂载/链接
- Network configuration,描述网络的JSON配置
- Extra arguments,基于容器级别的定制化配置参数
- Name of the interface inside the container,在容器网络命名空间中创建并分配的网络接口的名字,符合Linux接口命名空间,不得超过16字符,不含 / :或空白字符
结果:
- Interfaces list,取决于插件,返回的信息可能包括沙盒(容器或Hypervisor)接口名、宿主机接口名、接口的硬件地址u、沙盒的详细信息
- IP configuration assigned to each interface,分配到沙盒/宿主机的IPv4/IPv6地址、网关、路由
- DNS information,包含DNS信息(服务器、domain、search domains、options)的字典
该接口负责将容器从网络中移除。
参数:和ADD相同。
注意点:
- 所有参数必须和ADD时一致
- DEL必须清理掉容器持有的、在目标网络中的所有资源
- 如果存在已知的,针对容器的先前ADD操作,则运行时必须在插件(or all plugins in a chain)JSON中添加prevResult字段,该字段的内容是上一个ADD操作的结果,JSON形式
- 如果CNI_NETNS或/和prevResult没有提供,则插件应当尽可能清理更多的资源(例如释放IPAM分配的地址)并返回提示操作成功的响应
- 如果运行时缓存了针对容器的上一个ADD的结果,则它应该在成功DEL后清理掉此缓存
如果某些资源丢失,此接口一般也不返回错误。例如:
- 即使容器网络命名空间已不存在,IPAM插件也应该释放IP地址并且返回成功,除非网络命名空间对于IPAM管理是关键的
- bridge插件应当将DEL委托给IPAM插件,并清理自身的资源,即使网络命名空间/容器接口已不存在
该接口报告CNI插件相关的版本信息。没有参数,结果示例:
1 2 3 4 5 6 |
{ // 当前CNI版本 "cniVersion": "0.4.0", // 支持的CNI版本 "supportedVersions": [ "0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0" ] } |
检查容器的网络是否如预期一样。
参数:和ADD一样, 但是Network configuration包含prevResult字段,存放上一个ADD调用的结果。
结果:返回错误或空。
注意点:
- 插件必须检查prevResult,确定接口、地址符合预期
- 插件必须允许后续的chained plugin看到修改后的网络资源,例如路由
- 如果prevResult中列出的资源(接口、路由、地址)不存在,或者状态异常,应该返回错误
- 同样的,没有在prevResult中跟踪的网络资源,例如防火墙规则、流量塑形控制规则、IP预留、外部依赖,不存在或状态异常时,也应该返回错误
- 如果发现容器不可达,应单返回错误
- 插件需要处理在ADD后紧跟着的CHECK调用,这意味着需要考虑某些异步资源的合理创建延迟
- 插件需要调用所有被委托插件(例如IPAM)的CHECK,并将错误收集并返回
- 运行时不会对尚未ADD的容器调用CHECK,也不会对在ADD后进行DEL的容器进行CHECK
- 如果在配置列表中,disableCheck设置为true,则运行时不会调用CHECK
- 如果chain中一个插件返回错误,运行时可能选择停止继续迭代后续插件的CHECK
- 在ADD调用之后,直到DEL调用之前,运行时都可以调用CHECK
- 运行时可以假设CHECK失败的容器处于不可恢复的错误配置状态中
如果ADD操作成功,插件应该以0退出,并且在标准输出上打印JSON。JSON中ips、dns部分必须和IPAM插件输出的一致,interfaces数组则需要插件填写,因为IPAM插件对接口无感知:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
{ // 必须和Network Configuration中的一致 "cniVersion": "0.4.0", // IPAM插件忽略此键值 // 插件创建的接口列表 // 如果变量CNI_IFNAME存在,插件必须使用此名字来创建sandbox/hypervisor接口,如果不能,返回错误 "interfaces": [ { "name": "<name>", // 如果L2地址有价值,则必须 "mac": "<MAC address>", // 对于容器接口,必须是网络命名空间路径 // 对于虚拟机,必须是虚拟化沙盒的ID,网络接口在其中创建 // 对于宿主机接口,为空或忽略 "sandbox": "<netns path or hypervisor identifier>" } ], // IP配置信息 "ips": [ { "version": "<4-or-6>", "address": "<ip-and-prefix-in-CIDR>", "gateway": "<ip-address-of-the-gateway>", // 可选 "interface": <numeric-index-into-interfaces-list> }, ... ], // 路由配置信息 "routes": [ // 可选 { "dst": "<ip-and-prefix-in-cidr>", "gw": "<ip-of-next-hop>" // 可选 }, ... ], // DNS信息 "dns": { // 可选 "nameservers": <list-of-nameservers> "domain": <name-of-local-domain> "search": <list-of-additional-search-domains> "options": <list-of-options> } } |
如果ADD操作失败,则应该以非0退出,并打印:
1 2 3 4 5 6 |
{ "cniVersion": "0.4.0", "code": <numeric-error-code>, "msg": <short-error-message>, "details": <long-error-message> (optional) } |
网络配置为JSON格式,可以存放在磁盘上,或者由运行时从其它源动态生成。
插件可以定义自己的字段,知名字段包括:
字段 | 说明 |
cniVersion | 字符串。当前配置遵循的CNI规范版本 |
name | 字符串。网络名,在主机范围内必须唯一 |
type | 字符串。CNI插件的可执行文件的名字 |
args | 字典,可选。由运行时提供的额外参数。例如可以传递一组标签 |
ipMasq | 布尔,可选。如果插件支持,则在宿主机上为此网络设置IP遮掩(动态SNAT)。如果宿主机需要充当容器IP的网关,则为true |
ipam |
字典,可选。IPAM相关的配置: type:IPAM插件的可执行文件名 |
dns |
字典,可选。DNS相关配置: nameservers:此网络可以感知的,优先级排序的DNS服务器列表 |
插件可以定义额外的字段,如果配置文件中传入它不能理解的字段,可能会报错。例外是args字段,可以配置任何值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
{ "cniVersion": "0.4.0", "name": "dbnet", "type": "bridge", // 特有字段 "bridge": "cni0", "ipam": { "type": "host-local", // 特有字段 "subnet": "10.1.0.0/16", "gateway": "10.1.0.1" }, "dns": { "nameservers": [ "10.1.0.1" ] } } |
1 2 3 4 5 6 7 8 9 10 11 12 |
{ "cniVersion": "0.4.0", "name": "wan", "type": "macvlan", "ipam": { "type": "dhcp", "routes": [ { "dst": "10.0.0.0/8", "gw": "10.0.0.1" } ] }, "dns": { "nameservers": [ "10.0.0.1" ] } } |
使用网络配置列表,可以针对单个容器、按照特定顺序依次调用多个CNI插件,并将前一个插件的结果传递给后一个插件。
可用字段:
字段 | 说明 |
cniVersion | 字符串。当前配置遵循的CNI规范版本 |
name | 字符串。网络名,在主机范围内必须唯一 |
disableCheck | 布尔。如果为true则运行时不会调用CHECK |
plugins |
标准的CNI插件的配置列表 |
运行时调用单个插件时:
- 会将name, cniVersion换成列表中的name, cniVersion
- 会将上一个插件的结果写入到prevResult字段
- 在DEL时,调用插件的顺序和ADD相反
- 在相同环境下调用所有插件
- 如果出错,则终止后续插件调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
{ "name": "k8s-pod-network", "cniVersion": "0.3.0", "plugins": [ { "type": "calico", "log_level": "info", "etcd_endpoints": "https://10.0.5.1:2379,https://10.0.2.1:2379,https://10.0.3.1:2379", "etcd_key_file": "/etc/cni/net.d/calico-tls/etcd-key", "etcd_cert_file": "/etc/cni/net.d/calico-tls/etcd-cert", "etcd_ca_cert_file": "/etc/cni/net.d/calico-tls/etcd-ca", "mtu": 1440, "ipam": { "type": "calico-ipam" }, "policy": { "type": "k8s" }, "kubernetes": { "kubeconfig": "/etc/cni/net.d/calico-kubeconfig" } }, { "type": "portmap", "snat": true, "capabilities": {"portMappings": true} } ] } |
CNI插件可能需要为网络接口分配IP地址,并为网络接口安装相关的路由规则。为了支持不同的IP管理需求(DHCP、host-local),让IP分配和CNI插件基本功能解耦,规范定义了新的插件类型:IPAM Plugin。调用IPAM Plugin是CNI插件的职责,它应该在适当的时机发起调用,为网络接口获得IP地址。
IPAM插件需要决定:
- 网络接口IP/子网
- 网关
- 路由
并将这些信息返回给“主”CNI插件来应用。IPAM插件的信息来源可能是DHCP协议、本地文件系统中的数据、网络配置ipam段中的配置信息。
类似于CNI插件,IPAM插件也是通过运行可执行文件来调用的、参数也是通过stdin传递。 IPAM插件必须接收传递给CNI插件的全部环境变量。
如果调用成功,应当以0退出,并跟着如下格式的JSON:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
{ "cniVersion": "0.4.0", "ips": [ { "version": "<4-or-6>", "address": "<ip-and-prefix-in-CIDR>", // e.g. 192.168.1.3/24 "gateway": "<ip-address-of-the-gateway>" // optional }, ... ], "routes": [ // optional { "dst": "<ip-and-prefix-in-cidr>", "gw": "<ip-of-next-hop>" // optional }, ... ] "dns": { // optional "nameservers": <list-of-nameservers> // optional "domain": <name-of-local-domain> // optional "search": <list-of-search-domains> // optional "options": <list-of-options> // optional } } |
上述返回结果类似于CNI的返回结果,但是不包含interfaces键。
错误码 | 说明 |
1 | 不兼容的CNI版本 |
2 | 网络配置中包含不支持的字段,错误消息中应该指明不支持的键值 |
3 | 未知或不存在的容器,隐含运行时不需要进行任何容器网络清理 |
4 | 无效的必须环境变量,例如CNI_COMMAND、CNI_CONTAINERID |
5 | I/O错误,例如无法从stdin读取网络配置 |
6 | 无法解析网络配置 |
7 | 无效网络配置 |
11 | 出现临时性的错误,提示运行时后续重试 |
环境变量 | 说明 |
CNI_PATH | 从什么目录寻找CNI插件,默认 /opt/cni/bin |
CNI_CONTAINERID | 容器唯一标识 |
CNI_NETNS | 所在网络命名空间 |
CNI_IFNAME | 网络接口名称 |
CNI_COMMAND | 调用的CNI接口,例如ADD、DEL、CHECK |
CNI_ARGS | CNI参数 |
NETCONFPATH | CNI网络配置所在目录,默认 /etc/cni/net.d |
要开发自己的CNI插件,应当先去阅读CNI规范、样例/参考实现。
这是一个小工具,可以在执行CNI配置,在已存在的网络命名空间中添加、删除网络接口。
cnitool会搜索$NETCONFPATH下面所有的 *.conf或 *.json文件,加载它们,然后寻找网络名称和传递给cnitool匹配的网络配置。
1 2 |
go get github.com/containernetworking/cni go install github.com/containernetworking/cni/cnitool |
cnitool是参考实现的一部分,因此你可以直接构建参考实现,以获得cnitool:
1 2 3 |
git clone https://github.com/containernetworking/plugins.git cd plugins ./build_linux.sh |
首先需要创建网络命名空间:
1 2 3 |
sudo ip netns add testing # 自动创建/var/run/netns/testing,也就是/run/netns/testing # 此文件指向的inode即代表网络命名空间 |
并创建CNI网络配置:
1 2 3 4 5 6 7 8 9 10 11 |
{ "cniVersion": "0.4.0", "name": "myptp", "type": "ptp", "ipMasq": true, "ipam": { "type": "host-local", "subnet": "172.16.29.0/24", "routes": [ { "dst": "0.0.0.0/0" } ] } } |
然后调用cnitool:
1 |
sudo CNI_PATH=./bin cnitool add myptp /var/run/netns/testing |
检查配置是否符合预期:
1 |
sudo CNI_PATH=./bin cnitool check myptp /var/run/netns/testing |
检查命名空间中的网络接口:
1 2 |
sudo ip -n testing addr sudo ip netns exec testing ping -c 172.16.29.1 |
清理:
1 2 |
sudo CNI_PATH=./bin cnitool del myptp /var/run/netns/testing sudo ip netns del testing |
CNI项目提供了一个代码库,其中包含了若干参考实现。本文后面的内容会分析其中一些CNI插件的源代码。
构建、安装了参考实现,或者你自己编写的CNI插件后,可以利用参考实现项目的scripts/目录中的priv-net-run.sh、docker-run.sh来执行插件。
1 2 |
cd $GOPATH/src/github.com/containernetworking/plugins ./build_linux.sh |
你需要为被测试的CNI插件创建网络配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
{ "cniVersion": "0.2.0", "name": "mynet", "type": "bridge", "bridge": "cni0", "isGateway": true, "ipMasq": true, "ipam": { "type": "host-local", "subnet": "10.22.0.0/16", "routes": [ { "dst": "0.0.0.0/0" } ] } } |
1 2 3 4 5 |
{ "cniVersion": "0.2.0", "name": "lo", "type": "loopback" } |
调用命令:
1 2 3 4 5 6 7 8 9 |
CNI_PATH=$GOPATH/src/github.com/containernetworking/plugins/bin $GOPATH/src/github.com/containernetworking/cni/scripts sudo CNI_PATH=$CNI_PATH ./priv-net-run.sh ifconfig # eth0 Link encap:Ethernet HWaddr f2:c2:6f:54:b8:2b # inet addr:10.22.0.2 Bcast:0.0.0.0 Mask:255.255.0.0 # lo Link encap:Local Loopback # inet addr:127.0.0.1 Mask:255.0.0.0 |
你可以在私有网络命名空间下看到和上面两个CNI配置对应的网络接口。
创建并配置网络命名空间,然后在其中运行Docker容器:
1 2 3 |
CNI_PATH=$GOPATH/src/github.com/containernetworking/plugins/bin cd $GOPATH/src/github.com/containernetworking/cni/scripts sudo CNI_PATH=$CNI_PATH ./docker-run.sh --rm busybox:latest ifconfig |
这是CNI提供的一套接口,供容器运行时调用,实现CNI相关的操控。接口规格如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
type CNI interface { // 将容器添加到一组网络,也就是调用一组插件 AddNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) (types.Result, error) // 检查一组网络 CheckNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error // 将容器从一组网络中删除 DelNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error // 获取一组网络的ADD调用结果 GetNetworkListCachedResult(net *NetworkConfigList, rt *RuntimeConf) (types.Result, error) // 添加、检查、删除、获取单个网络 AddNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) (types.Result, error) CheckNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error DelNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error GetNetworkCachedResult(net *NetworkConfig, rt *RuntimeConf) (types.Result, error) // 进行配置校验 ValidateNetworkList(ctx context.Context, net *NetworkConfigList) ([]string, error) ValidateNetwork(ctx context.Context, net *NetworkConfig) ([]string, error) } |
NetworkConfigList、NetworkConfig就对应了规范中的网络配置列表、网络配置,JSON部分直接存放在字节数组:
1 2 3 4 5 6 7 8 9 10 11 12 |
type NetworkConfig struct { Network *types.NetConf Bytes []byte } type NetworkConfigList struct { Name string CNIVersion string DisableCheck bool Plugins []*NetworkConfig Bytes []byte } |
网络配置,转换为的结构:
1 2 3 4 5 6 7 8 9 10 11 12 |
type NetConf struct { CNIVersion string `json:"cniVersion,omitempty"` Name string `json:"name,omitempty"` Type string `json:"type,omitempty"` Capabilities map[string]bool `json:"capabilities,omitempty"` IPAM IPAM `json:"ipam,omitempty"` DNS DNS `json:"dns"` RawPrevResult map[string]interface{} `json:"prevResult,omitempty"` PrevResult Result `json:"-"` } |
RuntimeConf则包含了一次CNI调用中,除了网络配置之外的参数信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
type RuntimeConf struct { // 当前针对的容器ID ContainerID string // 当前操控的网络命名空间 NetNS string // 当前网络接口名 IfName string // 参数 Args [][2]string // 运行时传递给插件的Capability相关的数据 CapabilityArgs map[string]interface{} // 缓存目录 CacheDir string } |
dockershim是kubelet中的一个模块,Kubelet通过CRI gRPC调用进程内的dockershim,后者则将CRI请求适配为对Docker守护进程的请求。所以可以认为dockershim是一个容器运行时。
在创建Pod时,NetworkPlugin.SetUpPod方法会被调用,来为Pod安装网络:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
func (plugin *cniNetworkPlugin) SetUpPod(namespace string, name string, id kubecontainer.ContainerID, annotations, options map[string]string) error { if err := plugin.checkInitialized(); err != nil { return err } // 获得网络命名空间 netnsPath, err := plugin.host.GetNetNS(id.ID) if err != nil { return fmt.Errorf("CNI failed to retrieve network namespace path: %v", err) } cniTimeoutCtx, cancelFunc := context.WithTimeout(context.Background(), network.CNITimeoutSec*time.Second) defer cancelFunc() // Windows doesn't have loNetwork. It comes only with Linux if plugin.loNetwork != nil { if _, err = plugin.addToNetwork(cniTimeoutCtx, plugin.loNetwork, name, namespace, id, netnsPath, annotations, options); err != nil { return err } } // 将Pod加入到网络 _, err = plugin.addToNetwork(cniTimeoutCtx, plugin.getDefaultNetwork(), name, namespace, id, netnsPath, annotations, options) return err } func (plugin *cniNetworkPlugin) addToNetwork(ctx context.Context, network *cniNetwork, podName string, podNamespace string, podSandboxID kubecontainer.ContainerID, podNetnsPath string, annotations, options map[string]string) (cnitypes.Result, error) { // 生成RuntimeConfig rt, err := plugin.buildCNIRuntimeConf(podName, podNamespace, podSandboxID, podNetnsPath, annotations, options) if err != nil { klog.Errorf("Error adding network when building cni runtime conf: %v", err) return nil, err } pdesc := podDesc(podNamespace, podName, podSandboxID) netConf, cniNet := network.NetworkConfig, network.CNIConfig // 调用CNI接口 res, err := cniNet.AddNetworkList(ctx, netConf, rt) if err != nil { klog.Errorf("Error adding %s to network %s/%s: %v", pdesc, netConf.Plugins[0].Network.Type, netConf.Name, err) return nil, err } klog.V(4).Infof("Added %s to network %s: %v", pdesc, netConf.Name, res) return res, nil } |
参考实现的测试用例基于Ginkgo,本节阅读loopback插件的测试用例,依此了解如何调用、测试CNI插件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
package main_test import ( "github.com/onsi/gomega/gexec" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "testing" ) var pathToLoPlugin string func TestLoopback(t *testing.T) { RegisterFailHandler(Fail) // 入口点,启动测试套件 RunSpecs(t, "plugins/main/loopback") } var _ = BeforeSuite(func() { var err error // 使用go build来执行构建,返回临时目录中的二进制文件的路径 pathToLoPlugin, err = gexec.Build("github.com/containernetworking/plugins/plugins/main/loopback") Expect(err).NotTo(HaveOccurred()) }) var _ = AfterSuite(func() { // 清理套件启动后构建的二进制文件 gexec.CleanupBuildArtifacts() }) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
package main_test import ( "fmt" "net" "os/exec" "strings" "github.com/containernetworking/plugins/pkg/ns" "github.com/containernetworking/plugins/pkg/testutils" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/onsi/gomega/gbytes" "github.com/onsi/gomega/gexec" ) // 一组用例 var _ = Describe("Loopback", func() { var ( networkNS ns.NetNS command *exec.Cmd environ []string ) // 准备工作 BeforeEach(func() { // 插件命令 command = exec.Command(pathToLoPlugin) var err error // 通过unshare系统调用创建新NS,但是不会切换到此NS networkNS, err = testutils.NewNS() Expect(err).NotTo(HaveOccurred()) // 准备调用CNI插件需要的环境变量 environ = []string{ fmt.Sprintf("CNI_CONTAINERID=%s", "dummy"), // CNI插件需要在哪个命名空间中运作 fmt.Sprintf("CNI_NETNS=%s", networkNS.Path()), fmt.Sprintf("CNI_IFNAME=%s", "this is ignored"), fmt.Sprintf("CNI_ARGS=%s", "none"), fmt.Sprintf("CNI_PATH=%s", "/some/test/path"), } // 准备插件输入参数 command.Stdin = strings.NewReader(`{ "name": "loopback-test", "cniVersion": "0.1.0" }`) }) // 清理工作,关闭命名空间描述符、卸载命名空间 AfterEach(func() { Expect(networkNS.Close()).To(Succeed()) Expect(testutils.UnmountNS(networkNS)).To(Succeed()) }) Context("when given a network namespace", func() { It("sets the lo device to UP", func() { // 调用ADD接口 command.Env = append(environ, fmt.Sprintf("CNI_COMMAND=%s", "ADD")) session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) // 对命令的标准输出进行断言 Eventually(session).Should(gbytes.Say(`{.*}`)) Eventually(session).Should(gexec.Exit(0)) var lo *net.Interface err = networkNS.Do(func(ns.NetNS) error { // 如果CNI插件运行成功,则在此NS中会出现一个lo网卡 var err error lo, err = net.InterfaceByName("lo") return err }) Expect(err).NotTo(HaveOccurred()) Expect(lo.Flags & net.FlagUp).To(Equal(net.FlagUp)) }) It("sets the lo device to DOWN", func() { // 调用DEL接口 command.Env = append(environ, fmt.Sprintf("CNI_COMMAND=%s", "DEL")) session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) Eventually(session).Should(gbytes.Say(``)) Eventually(session).Should(gexec.Exit(0)) var lo *net.Interface err = networkNS.Do(func(ns.NetNS) error { // 如果CNI插件运行成功,则在此NS中不存在lo网卡 var err error lo, err = net.InterfaceByName("lo") return err }) Expect(err).NotTo(HaveOccurred()) Expect(lo.Flags & net.FlagUp).NotTo(Equal(net.FlagUp)) }) }) }) |
这个包提供了CNI插件开发的骨架代码,它实现了参数解析和校验,你可以将其作为库使用,简化CNI开发。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 |
package skel import ( "bytes" "encoding/json" "fmt" "io" "io/ioutil" "log" "os" "strings" "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/version" ) // 来自环境变量、标准输入的CNI参数 type CmdArgs struct { ContainerID string Netns string IfName string Args string Path string StdinData []byte } // 解析并得到CmdArgs、执行CNI调用 type dispatcher struct { Getenv func(string) string Stdin io.Reader Stdout io.Writer Stderr io.Writer ConfVersionDecoder version.ConfigDecoder VersionReconciler version.Reconciler } type reqForCmdEntry map[string]bool // internal only error to indicate lack of required environment variables type missingEnvError struct { msg string } func (e missingEnvError) Error() string { return e.msg } // 从环境变量解析参数 func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, error) { var cmd, contID, netns, ifName, args, path string vars := []struct { name string val *string reqForCmd reqForCmdEntry }{ { "CNI_COMMAND", // 接口名 &cmd, reqForCmdEntry{ "ADD": true, "CHECK": true, "DEL": true, }, }, { "CNI_CONTAINERID", // 容器ID &contID, reqForCmdEntry{ "ADD": true, "CHECK": true, "DEL": true, }, }, { "CNI_NETNS", // 网络NS文件路径 &netns, reqForCmdEntry{ "ADD": true, "CHECK": true, "DEL": false, }, }, { "CNI_IFNAME", // 网络接口名 &ifName, reqForCmdEntry{ "ADD": true, "CHECK": true, "DEL": true, }, }, { "CNI_ARGS", // CNI参数 &args, reqForCmdEntry{ "ADD": false, "CHECK": false, "DEL": false, }, }, { "CNI_PATH", // CNI二进制文件路径 &path, reqForCmdEntry{ "ADD": true, "CHECK": true, "DEL": true, }, }, } argsMissing := make([]string, 0) for _, v := range vars { *v.val = t.Getenv(v.name) if *v.val == "" { if v.reqForCmd[cmd] || v.name == "CNI_COMMAND" { argsMissing = append(argsMissing, v.name) } } } if len(argsMissing) > 0 { joined := strings.Join(argsMissing, ",") return "", nil, missingEnvError{fmt.Sprintf("required env variables [%s] missing", joined)} } if cmd == "VERSION" { t.Stdin = bytes.NewReader(nil) } stdinData, err := ioutil.ReadAll(t.Stdin) if err != nil { return "", nil, fmt.Errorf("error reading from stdin: %v", err) } cmdArgs := &CmdArgs{ ContainerID: contID, Netns: netns, IfName: ifName, Args: args, Path: path, StdinData: stdinData, } return cmd, cmdArgs, nil } func createTypedError(f string, args ...interface{}) *types.Error { return &types.Error{ Code: 100, Msg: fmt.Sprintf(f, args...), } } // 检查CNI并执行调用 func (t *dispatcher) checkVersionAndCall(cmdArgs *CmdArgs, pluginVersionInfo version.PluginInfo, toCall func(*CmdArgs) error) error { configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData) if err != nil { return err } verErr := t.VersionReconciler.Check(configVersion, pluginVersionInfo) if verErr != nil { return &types.Error{ Code: types.ErrIncompatibleCNIVersion, Msg: "incompatible CNI versions", Details: verErr.Details(), } } return toCall(cmdArgs) } // 校验配置 func validateConfig(jsonBytes []byte) error { var conf struct { Name string `json:"name"` } if err := json.Unmarshal(jsonBytes, &conf); err != nil { return fmt.Errorf("error reading network config: %s", err) } if conf.Name == "" { return fmt.Errorf("missing network name") } return nil } // CNI调用的核心逻辑 func (t *dispatcher) pluginMain(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) *types.Error { // 解析命令和参数 cmd, cmdArgs, err := t.getCmdArgsFromEnv() if err != nil { // 如果没有指定命令,则打印About后退出 if _, ok := err.(missingEnvError); ok && t.Getenv("CNI_COMMAND") == "" && about != "" { fmt.Fprintln(t.Stderr, about) return nil } return createTypedError(err.Error()) } // 进行配置(标准输入)校验 if cmd != "VERSION" { err = validateConfig(cmdArgs.StdinData) if err != nil { return createTypedError(err.Error()) } } switch cmd { case "ADD": // ADD接口,直接转调函数 err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdAdd) case "CHECK": // CHECK接口 // 检查版本 configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData) if err != nil { return createTypedError(err.Error()) } if gtet, err := version.GreaterThanOrEqualTo(configVersion, "0.4.0"); err != nil { return createTypedError(err.Error()) } else if !gtet { return &types.Error{ Code: types.ErrIncompatibleCNIVersion, Msg: "config version does not allow CHECK", } } for _, pluginVersion := range versionInfo.SupportedVersions() { gtet, err := version.GreaterThanOrEqualTo(pluginVersion, configVersion) if err != nil { return createTypedError(err.Error()) } else if gtet { // 转调函数 if err := t.checkVersionAndCall(cmdArgs, versionInfo, cmdCheck); err != nil { return createTypedError(err.Error()) } return nil } } return &types.Error{ Code: types.ErrIncompatibleCNIVersion, Msg: "plugin version does not allow CHECK", } case "DEL": // DEL接口,直接转调函数 err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdDel) case "VERSION": // 打印版本信息 err = versionInfo.Encode(t.Stdout) default: return createTypedError("unknown CNI_COMMAND: %v", cmd) } if err != nil { if e, ok := err.(*types.Error); ok { // don't wrap Error in Error return e } return createTypedError(err.Error()) } return nil } // PluginMainWithError是一个CNI的核心main逻辑骨架,调用后产生的错误需要你自行处理 // 实际上就是创建dispatcher,并调用它的pluginMain方法 func PluginMainWithError(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) *types.Error { return (&dispatcher{ Getenv: os.Getenv, Stdin: os.Stdin, Stdout: os.Stdout, Stderr: os.Stderr, }).pluginMain(cmdAdd, cmdCheck, cmdDel, versionInfo, about) } // 此函数是一个CNI的核心main逻辑骨架,支持自动化的错误处理 // // 调用者(CNI插件)需要指明它支持哪些CNI规范版本 // // 如果没有指定CNI_COMMAND,则调用者提供的about打印到stderr // about的推荐格式: CNI plugin <foo> v<version> // // cmdAdd, cmdCheck或cmdDel调用出错,则PluginMain打印错误JSON到stdout,并os.Exit(1) // // 如果需要定制错误处理,调用PluginMainWithError() func PluginMain(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) { if e := PluginMainWithError(cmdAdd, cmdCheck, cmdDel, versionInfo, about); e != nil { if err := e.Print(); err != nil { log.Print("Error writing error JSON to stdout: ", err) } os.Exit(1) } } |
有了skel,我们实现自己的CNI插件时,只需要编写几个函数就可以了。
这个插件很简单,就是在命名空间中添加一个lo接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 |
package main import ( "encoding/json" "errors" "fmt" "net" "github.com/vishvananda/netlink" "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/version" "github.com/containernetworking/plugins/pkg/ns" bv "github.com/containernetworking/plugins/pkg/utils/buildversion" ) // 从标准输入的JSON解析CNI配置 func parseNetConf(bytes []byte) (*types.NetConf, error) { conf := &types.NetConf{} if err := json.Unmarshal(bytes, conf); err != nil { return nil, fmt.Errorf("failed to parse network config: %v", err) } if conf.RawPrevResult != nil { if err := version.ParsePrevResult(conf); err != nil { return nil, fmt.Errorf("failed to parse prevResult: %v", err) } if _, err := current.NewResultFromResult(conf.PrevResult); err != nil { return nil, fmt.Errorf("failed to convert result to current version: %v", err) } } return conf, nil } func cmdAdd(args *skel.CmdArgs) error { // 解析出配置 conf, err := parseNetConf(args.StdinData) if err != nil { return err } var v4Addr, v6Addr *net.IPNet // 强制使用lo作为网络接口名称,忽略配置 args.IfName = "lo" // 在路径args.Netns代表的网络命名空间下执行 err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error { // 获取Link对象,lo接口不需要添加,默认就存在,只是可能没有up link, err := netlink.LinkByName(args.IfName) if err != nil { return err // not tested } // ip link set $link up err = netlink.LinkSetUp(link) if err != nil { return err // not tested } // 获得link的IP地址接口 v4Addrs, err := netlink.AddrList(link, netlink.FAMILY_V4) if err != nil { return err // not tested } // 如果有IPv4的地址,则确认是loopback地址 if len(v4Addrs) != 0 { v4Addr = v4Addrs[0].IPNet // sanity check that this is a loopback address for _, addr := range v4Addrs { if !addr.IP.IsLoopback() { return fmt.Errorf("loopback interface found with non-loopback address %q", addr.IP) } } } // 如果有IPv6的地址,则确认是loopback地址 v6Addrs, err := netlink.AddrList(link, netlink.FAMILY_V6) if err != nil { return err // not tested } if len(v6Addrs) != 0 { v6Addr = v6Addrs[0].IPNet // sanity check that this is a loopback address for _, addr := range v6Addrs { if !addr.IP.IsLoopback() { return fmt.Errorf("loopback interface found with non-loopback address %q", addr.IP) } } } return nil }) if err != nil { return err // not tested } var result types.Result // 如果有来自上一个CNI插件的结果,则透明的传递下去 if conf.PrevResult != nil { // If loopback has previous result which passes from previous CNI plugin, // loopback should pass it transparently result = conf.PrevResult } else { // 否则,构建一个结果 loopbackInterface := ¤t.Interface{Name: args.IfName, Mac: "00:00:00:00:00:00", Sandbox: args.Netns} r := ¤t.Result{CNIVersion: conf.CNIVersion, Interfaces: []*current.Interface{loopbackInterface}} if v4Addr != nil { r.IPs = append(r.IPs, ¤t.IPConfig{ Version: "4", Interface: current.Int(0), Address: *v4Addr, }) } if v6Addr != nil { r.IPs = append(r.IPs, ¤t.IPConfig{ Version: "6", Interface: current.Int(0), Address: *v6Addr, }) } result = r } return types.PrintResult(result, conf.CNIVersion) } // 只是把lo关闭,不能删除 func cmdDel(args *skel.CmdArgs) error { if args.Netns == "" { return nil } args.IfName = "lo" // ignore config, this only works for loopback err := ns.WithNetNSPath(args.Netns, func(ns.NetNS) error { link, err := netlink.LinkByName(args.IfName) if err != nil { return err // not tested } err = netlink.LinkSetDown(link) if err != nil { return err // not tested } return nil }) if err != nil { return err // not tested } return nil } // 检查lo接口是否启动 func cmdCheck(args *skel.CmdArgs) error { args.IfName = "lo" // ignore config, this only works for loopback return ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error { link, err := netlink.LinkByName(args.IfName) if err != nil { return err } // link属性标记,位域 if link.Attrs().Flags&net.FlagUp != net.FlagUp { return errors.New("loopback interface is down") } return nil }) } func main() { // 调用skel skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("loopback")) } |
使用该插件,同一主机上的所有容器被连接到一个网桥上,网桥位于宿主机的网络命名空间中。
一个veth对,一端接着网桥,另一端接着容器。
IP地址仅仅在veth对连接容器的那一端分配。
网桥自身也可以分配IP地址,这样它可以作为所有容器的网关。如果不分配,那么网桥工作在纯L2模式,如果容器有对外访问需求(也就是不仅仅需要和其它容器通信),则网桥需要桥接到宿主机的某个网络接口。
使用此插件时,需要指定网桥的名字。示例配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
{ "cniVersion": "0.3.1", "name": "mynet", "type": "bridge", // 使用或创建的网桥,默认cni0 "bridge": "mynet0", // 如果为true则为网桥分配IP地址,默认false "isGateway": true, // isGateway为true的前提下,如果设置为true,则设置网桥IP为默认路由 "isDefaultGateway": true, // 提示如果前值发生变化,是否需要设置新的IP地址,默认false "forceAddress": false, // 在宿主机上,为来自此容器,目标是主机之外的流量设置IP遮掩(SNAT),默认值由内核选择 "ipMasq": true, // 为网桥上的接口设置hairpin模式,默认false // 默认情况下,网桥不会将从端口A发来的封包,再发回A去,这会导致容器无法访问自己 "hairpinMode": true, // 使用的IPAM插件 "ipam": { "type": "host-local", "subnet": "10.10.0.0/16" } // 将网桥设置为混杂模式,默认false // 混杂模式下,网络接口监听任何封包,不管它的目的地(MAC)是不是自己 promiscMode: false // 分配VLAN tag,默认不分配 // 如果指定,则在veth对的宿主机端设置VLAN tag,并且启用网桥的vlan_filtering特性 vlan: 0 } |
纯L2模式配置:
1 2 3 4 5 6 7 |
{ "cniVersion": "0.3.1", "name": "mynet", "type": "bridge", "bridge": "mynet0", "ipam": {} } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 |
package main import ( "encoding/json" "errors" "fmt" "io/ioutil" "net" "runtime" "syscall" "time" "github.com/j-keck/arping" "github.com/vishvananda/netlink" "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/version" "github.com/containernetworking/plugins/pkg/ip" "github.com/containernetworking/plugins/pkg/ipam" "github.com/containernetworking/plugins/pkg/ns" "github.com/containernetworking/plugins/pkg/utils" bv "github.com/containernetworking/plugins/pkg/utils/buildversion" "github.com/containernetworking/plugins/pkg/utils/sysctl" ) // For testcases to force an error after IPAM has been performed var debugPostIPAMError error const defaultBrName = "cni0" type NetConf struct { types.NetConf BrName string `json:"bridge"` IsGW bool `json:"isGateway"` IsDefaultGW bool `json:"isDefaultGateway"` ForceAddress bool `json:"forceAddress"` IPMasq bool `json:"ipMasq"` MTU int `json:"mtu"` HairpinMode bool `json:"hairpinMode"` PromiscMode bool `json:"promiscMode"` Vlan int `json:"vlan"` } // 网关信息 type gwInfo struct { // IP地址和子网掩码 gws []net.IPNet // IPv4还是IPv6 family int // 在网关列表中,到此网关为止,是否发现默认路由 defaultRouteFound bool } func init() { // 确保main函数在主线程(线程组Leader)上运行 // 命名空间操作(unshare、setns)是为单个线程执行的,我们需要确保main函数 // 不会跳转到其它OS线程上 runtime.LockOSThread() } func loadNetConf(bytes []byte) (*NetConf, string, error) { n := &NetConf{ BrName: defaultBrName, } if err := json.Unmarshal(bytes, n); err != nil { return nil, "", fmt.Errorf("failed to load netconf: %v", err) } if n.Vlan < 0 || n.Vlan > 4094 { return nil, "", fmt.Errorf("invalid VLAN ID %d (must be between 0 and 4094)", n.Vlan) } return n, n.CNIVersion, nil } // 处理IPAM插件的结果,为每个地址族: // 1. 计算、编译网关地址列表 // 2. 如果需要,添加一个默认路由 func calcGateways(result *current.Result, n *NetConf) (*gwInfo, *gwInfo, error) { gwsV4 := &gwInfo{} gwsV6 := &gwInfo{} for _, ipc := range result.IPs { // Determine if this config is IPv4 or IPv6 var gws *gwInfo defaultNet := &net.IPNet{} switch { case ipc.Address.IP.To4() != nil: gws = gwsV4 gws.family = netlink.FAMILY_V4 defaultNet.IP = net.IPv4zero case len(ipc.Address.IP) == net.IPv6len: gws = gwsV6 gws.family = netlink.FAMILY_V6 defaultNet.IP = net.IPv6zero default: return nil, nil, fmt.Errorf("Unknown IP object: %v", ipc) } defaultNet.Mask = net.IPMask(defaultNet.IP) // All IPs currently refer to the container interface ipc.Interface = current.Int(2) // 如果没有提供,则计算出网关地址 if ipc.Gateway == nil && n.IsGW { ipc.Gateway = calcGatewayIP(&ipc.Address) } // 如有必要,为当前地址族添加默认路由(使用当前网关地址) if n.IsDefaultGW && !gws.defaultRouteFound { for _, route := range result.Routes { if route.GW != nil && defaultNet.String() == route.Dst.String() { gws.defaultRouteFound = true break } } if !gws.defaultRouteFound { // 添加一条默认路由 result.Routes = append( result.Routes, &types.Route{Dst: *defaultNet, GW: ipc.Gateway}, ) gws.defaultRouteFound = true }SetupVeth } // Append this gateway address to the list of gateways if n.IsGW { gw := net.IPNet{ IP: ipc.Gateway, Mask: ipc.Address.Mask, } gws.gws = append(gws.gws, gw) } } return gwsV4, gwsV6, nil } // 确保IP地址被设置到link上 func ensureAddr(br netlink.Link, family int, ipn *net.IPNet, forceAddress bool) error { // 现有地址列表 addrs, err := netlink.AddrList(br, family) if err != nil && err != syscall.ENOENT { return fmt.Errorf("could not get list of IP addresses: %v", err) } // 需要被设置的IP地址 ipnStr := ipn.String() for _, a := range addrs { // 已经被设置了,不做操作 if a.IPNet.String() == ipnStr { return nil } // 如果对应的子网不重叠,多个IPv6地址可以存在于网桥上 // 对于IPv4,或者子网重叠的Ipv6,如果forceAddress为true,重新配置IP地址 if family == netlink.FAMILY_V4 || a.IPNet.Contains(ipn.IP) || ipn.Contains(a.IPNet.IP) { if forceAddress { // 删除现有IP地址 if err = deleteAddr(br, a.IPNet); err != nil { return err } } else { return fmt.Errorf("%q already has an IP address different from %v", br.Attrs().Name, ipnStr) } } } // 添加新的IP地址 addr := &netlink.Addr{IPNet: ipn, Label: ""} if err := netlink.AddrAdd(br, addr); err != nil && err != syscall.EEXIST { return fmt.Errorf("could not add IP address to %q: %v", br.Attrs().Name, err) } // 将网桥的MAC地址设置给自己,否则网桥将使用小写版本的MAC if err := netlink.LinkSetHardwareAddr(br, br.Attrs().HardwareAddr); err != nil { return fmt.Errorf("could not set bridge's mac: %v", err) } return nil } // 删除IP地址 func deleteAddr(br netlink.Link, ipn *net.IPNet) error { addr := &netlink.Addr{IPNet: ipn, Label: ""} if err := netlink.AddrDel(br, addr); err != nil { return fmt.Errorf("could not remove IP address from %q: %v", br.Attrs().Name, err) } return nil } // 根据名称获取网桥 func bridgeByName(name string) (*netlink.Bridge, error) { l, err := netlink.LinkByName(name) if err != nil { return nil, fmt.Errorf("could not lookup %q: %v", name, err) } // 通过CAST判断 br, ok := l.(*netlink.Bridge) if !ok { return nil, fmt.Errorf("%q already exists but is not a bridge", name) } return br, nil } // 确保网桥存在、配置好 func ensureBridge(brName string, mtu int, promiscMode, vlanFiltering bool) (*netlink.Bridge, error) { // 网桥对象 br := &netlink.Bridge{ LinkAttrs: netlink.LinkAttrs{ Name: brName, MTU: mtu, // 让内核使用默认的txqueuelen; leaving it unset // means 0, and a zero-length TX queue messes up FIFO // traffic shapers which use TX queue length as the // default packet limit TxQLen: -1, }, } if vlanFiltering { // 启用VLAN过滤 br.VlanFiltering = &vlanFiltering } // 添加网桥 err := netlink.LinkAdd(br) if err != nil && err != syscall.EEXIST { return nil, fmt.Errorf("could not add %q: %v", brName, err) } // 设置混杂模式 if promiscMode { if err := netlink.SetPromiscOn(br); err != nil { return nil, fmt.Errorf("could not set promiscuous mode on %q: %v", brName, err) } } // 重新读取网桥配置 br, err = bridgeByName(brName) if err != nil { return nil, err } // we want to own the routes for this interface _, _ = sysctl.Sysctl(fmt.Sprintf("net/ipv6/conf/%s/accept_ra", brName), "0") // 启动网桥 if err := netlink.LinkSetUp(br); err != nil { return nil, err } return br, nil } // 确保VLAN接口就绪 func ensureVlanInterface(br *netlink.Bridge, vlanId int) (netlink.Link, error) { name := fmt.Sprintf("%s.%d", br.Name, vlanId) // 检查是否已经存在 brGatewayVeth, err := netlink.LinkByName(name) if err != nil { if err.Error() != "Link not found" { return nil, fmt.Errorf("failed to find interface %q: %v", name, err) } hostNS, err := ns.GetCurrentNS() if err != nil { return nil, fmt.Errorf("faild to find host namespace: %v", err) } // 创建veth对,一端连在网桥,另外一端连接在hostNS的name网络接口(VLAN接口) _, brGatewayIface, err := setupVeth(hostNS, br, name, br.MTU, false, vlanId) if err != nil { return nil, fmt.Errorf("faild to create vlan gateway %q: %v", name, err) } brGatewayVeth, err = netlink.LinkByName(brGatewayIface.Name) if err != nil { return nil, fmt.Errorf("failed to lookup %q: %v", brGatewayIface.Name, err) } } return brGatewayVeth, nil } // 创建veth对 func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool, vlanID int) (*current.Interface, *current.Interface, error) { contIface := ¤t.Interface{} hostIface := ¤t.Interface{} err := netns.Do(func(hostNS ns.NetNS) error { // 创建veth对,在容器里调用,会自动将对端移动到hostNS hostVeth, containerVeth, err := ip.SetupVeth(ifName, mtu, hostNS) if err != nil { return err } contIface.Name = containerVeth.Name contIface.Mac = containerVeth.HardwareAddr.String() contIface.Sandbox = netns.Path() hostIface.Name = hostVeth.Name return nil }) if err != nil { return nil, nil, err } // 由于移动了NS,hostVeth的index已经变了,需要重新获取 hostVeth, err := netlink.LinkByName(hostIface.Name) if err != nil { return nil, nil, fmt.Errorf("failed to lookup %q: %v", hostIface.Name, err) } hostIface.Mac = hostVeth.Attrs().HardwareAddr.String() // 将hostVeth对接到网桥上 if err := netlink.LinkSetMaster(hostVeth, br); err != nil { return nil, nil, fmt.Errorf("failed to connect %q to bridge %v: %v", hostVeth.Attrs().Name, br.Attrs().Name, err) } // 设置Hairpin模式 if err = netlink.LinkSetHairpin(hostVeth, hairpinMode); err != nil { return nil, nil, fmt.Errorf("failed to setup hairpin mode for %v: %v", hostVeth.Attrs().Name, err) }SetupVeth // 如果指定了VLAN,则在网桥上(实际上是VETH的宿主机端,桥的一个端口)添加VLAN filter entry if vlanID != 0 { err = netlink.BridgeVlanAdd(hostVeth, uint16(vlanID), true, true, false, true) if err != nil { return nil, nil, fmt.Errorf("failed to setup vlan tag on interface %q: %v", hostIface.Name, err) } } return hostIface, contIface, nil } // 网关IP,简单的+1 func calcGatewayIP(ipn *net.IPNet) net.IP { nid := ipn.IP.Mask(ipn.Mask) return ip.NextIP(nid) } // 创建网桥 func setupBridge(n *NetConf) (*netlink.Bridge, *current.Interface, error) { vlanFiltering := false if n.Vlan != 0 { vlanFiltering = true } // create bridge if necessary br, err := ensureBridge(n.BrName, n.MTU, n.PromiscMode, vlanFiltering) if err != nil { return nil, nil, fmt.Errorf("failed to create bridge %q: %v", n.BrName, err) } return br, ¤t.Interface{ Name: br.Attrs().Name, Mac: br.Attrs().HardwareAddr.String(), }, nil } // disableIPV6DAD disables IPv6 Duplicate Address Detection (DAD) // for an interface, if the interface does not support enhanced_dad. // We do this because interfaces with hairpin mode will see their own DAD packets func disableIPV6DAD(ifName string) error { // ehanced_dad sends a nonce with the DAD packets, so that we can safely // ignore ourselves enh, err := ioutil.ReadFile(fmt.Sprintf("/proc/sys/net/ipv6/conf/%s/enhanced_dad", ifName)) if err == nil && string(enh) == "1\n" { return nil } f := fmt.Sprintf("/proc/sys/net/ipv6/conf/%s/accept_dad", ifName) return ioutil.WriteFile(f, []byte("0"), 0644) } // 启用IP转发 func enableIPForward(family int) error { if family == netlink.FAMILY_V4 { return ip.EnableIP4Forward() } return ip.EnableIP6Forward() } func cmdAdd(args *skel.CmdArgs) error { var success bool = false // 解析配置 n, cniVersion, err := loadNetConf(args.StdinData) if err != nil { return err } if n.IsDefaultGW { n.IsGW = true } // 不支持同时设置hairpin和promisc模式 if n.HairpinMode && n.PromiscMode { return fmt.Errorf("cannot set hairpin mode and promiscous mode at the same time.") } // 如有必要,创建网桥,并进行配置 br, brInterface, err := setupBridge(n) if err != nil { return err } netns, err := ns.GetNS(args.Netns) if err != nil { return fmt.Errorf("failed to open netns %q: %v", args.Netns, err) } defer netns.Close() // 创建VETH对,一端插入容器,成为containerInterface // 另外一端插到网桥上,即hostInterface hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode, n.Vlan) if err != nil { return err } // Assume L2 interface only result := ¤t.Result{CNIVersion: cniVersion, Interfaces: []*current.Interface{brInterface, hostInterface, containerInterface}} isLayer3 := n.IPAM.Type != "" // 如果是L3模式,向IPAM申请IP地址 if isLayer3 { // 调用IPAM插件,传递自己的配置给它 r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData) if err != nil { return err } // 如果调用失败,时放掉可能已经分配的IP地址 defer func() { if !success { ipam.ExecDel(n.IPAM.Type, args.StdinData) } }() // Convert whatever the IPAM result was into the current Result type ipamResult, err := current.NewResultFromResult(r) if err != nil { return err } // 获取IPAM分配的地址、路由 result.IPs = ipamResult.IPs result.Routes = ipamResult.Routes if len(result.IPs) == 0 { return errors.New("IPAM plugin returned missing IP config") } // 为每个地址族收集路由信息 gwsV4, gwsV6, err := calcGateways(result, n) if err != nil { return err } // 在容器端配置IP地址和路由 if err := netns.Do(func(_ ns.NetNS) error { // 为了应对网桥开启hairpin的情况,禁用DAD // Hairpin会导致回响(echo)neighbor solicitation packets, 进而导致DAD错误 for _, ipc := range result.IPs { if ipc.Version == "6" && (n.HairpinMode || n.PromiscMode) { // 禁用IPv6 Duplicate Address Detection if err := disableIPV6DAD(args.IfName); err != nil { return err } break } } // 使用IPAM的调用结果来分配IP地址给容器接口 if err := ipam.ConfigureIface(args.IfName, result); err != nil { return err } return nil }); err != nil { return err } // 检查网桥Port状态 retries := []int{0, 50, 500, 1000, 1000} for idx, sleep := range retries { time.Sleep(time.Duration(sleep) * time.Millisecond) // 宿主机端(连接在网桥上)的VETH端 hostVeth, err := netlink.LinkByName(hostInterface.Name) if err != nil { return err } // 应当处于Up状态(可以发封包了) if hostVeth.Attrs().OperState == netlink.OperUp { break } if idx == len(retries)-1 { return fmt.Errorf("bridge port in error state: %s", hostVeth.Attrs().OperState) } } // 发送无故ARP请求(gratuitous arp),请求容器接口自身IP地址对应的MAC,价值是: // 1. 验证IP是否冲突,如果此ARP收到应答,则提示网络中有人用了和自己相同的IP // 2. 如果当前接口改变了MAC地址,可以通知网络中其它主机及时更新ARP缓存 if err := netns.Do(func(_ ns.NetNS) error { contVeth, err := net.InterfaceByName(args.IfName) if err != nil { return err } for _, ipc := range result.IPs { if ipc.Version == "4" { _ = arping.GratuitousArpOverIface(ipc.Address.IP, *contVeth) } } return nil }); err != nil { return err } // 网关模式下,为网桥配置IP地址,并启用IP转发 if n.IsGW { var firstV4Addr net.IP var vlanInterface *current.Interface // 遍历IPAM分配的网关信息 for _, gws := range []*gwInfo{gwsV4, gwsV6} { for _, gw := range gws.gws { if gw.IP.To4() != nil && firstV4Addr == nil { firstV4Addr = gw.IP } if n.Vlan != 0 { // 如有必要,创建名为brname.vlanid的VLAN接口 vlanIface, err := ensureVlanInterface(br, n.Vlan) if err != nil { return fmt.Errorf("failed to create vlan interface: %v", err) } if vlanInterface == nil { vlanInterface = ¤t.Interface{Name: vlanIface.Attrs().Name, Mac: vlanIface.Attrs().HardwareAddr.String()} result.Interfaces = append(result.Interfaces, vlanInterface) } // 将IP分配给VLAN接口 err = ensureAddr(vlanIface, gws.family, &gw, n.ForceAddress) if err != nil { return fmt.Errorf("failed to set vlan interface for bridge with addr: %v", err) } } else { // 将IP分配给网桥 err = ensureAddr(br, gws.family, &gw, n.ForceAddress) if err != nil { return fmt.Errorf("failed to set bridge addr: %v", err) } } } if gws.gws != nil { // 启用宿主机的IP转发功能 if err = enableIPForward(gws.family); err != nil { return fmt.Errorf("failed to enable forwarding: %v", err) } } } } if n.IPMasq { // 设置iptables规则,以实现MASQ chain := utils.FormatChainName(n.Name, args.ContainerID) comment := utils.FormatComment(n.Name, args.ContainerID) for _, ipc := range result.IPs { if err = ip.SetupIPMasq(&ipc.Address, chain, comment); err != nil { return err } } } } // 在第一个VETH添加之后,或者设置IP地址之后,网桥的MAC可能变化了 // 因此重新获取网桥 br, err = bridgeByName(n.BrName) if err != nil { return err } brInterface.Mac = br.Attrs().HardwareAddr.String() result.DNS = n.DNS // Return an error requested by testcases, if any if debugPostIPAMError != nil { return debugPostIPAMError } success = true return types.PrintResult(result, cniVersion) } func cmdDel(args *skel.CmdArgs) error { n, _, err := loadNetConf(args.StdinData) if err != nil { return err } isLayer3 := n.IPAM.Type != "" if isLayer3 { // 回收IP地址 if err := ipam.ExecDel(n.IPAM.Type, args.StdinData); err != nil { return err } } if args.Netns == "" { return nil } // 网络命名空间存在,尝试清理其中的网络接口,由于可能调用多次 // 因此需要实现幂等操作,如果网络接口已经被移除过,不要返回错误 var ipnets []*net.IPNet err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error { var err error // 删除link ipnets, err = ip.DelLinkByNameAddr(args.IfName) if err != nil && err == ip.ErrLinkNotFound { return nil } return err }) if err != nil { return err } // 清理iptables规则 if isLayer3 && n.IPMasq { chain := utils.FormatChainName(n.Name, args.ContainerID) comment := utils.FormatComment(n.Name, args.ContainerID) for _, ipn := range ipnets { if err := ip.TeardownIPMasq(ipn, chain, comment); err != nil { return err } } } return err } func main() { skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("bridge")) } type cniBridgeIf struct { Name string ifIndex int peerIndex int masterIndex int found bool } func validateInterface(intf current.Interface, expectInSb bool) (cniBridgeIf, netlink.Link, error) { ifFound := cniBridgeIf{found: false} if intf.Name == "" { return ifFound, nil, fmt.Errorf("Interface name missing ") } link, err := netlink.LinkByName(intf.Name) if err != nil { return ifFound, nil, fmt.Errorf("Interface name %s not found", intf.Name) } if expectInSb { if intf.Sandbox == "" { return ifFound, nil, fmt.Errorf("Interface %s is expected to be in a sandbox", intf.Name) } } else { if intf.Sandbox != "" { return ifFound, nil, fmt.Errorf("Interface %s should not be in sandbox", intf.Name) } } return ifFound, link, err } func validateCniBrInterface(intf current.Interface, n *NetConf) (cniBridgeIf, error) { brFound, link, err := validateInterface(intf, false) if err != nil { return brFound, err } _, isBridge := link.(*netlink.Bridge) if !isBridge { return brFound, fmt.Errorf("Interface %s does not have link type of bridge", intf.Name) } if intf.Mac != "" { if intf.Mac != link.Attrs().HardwareAddr.String() { return brFound, fmt.Errorf("Bridge interface %s Mac doesn't match: %s", intf.Name, intf.Mac) } } linkPromisc := link.Attrs().Promisc != 0 if linkPromisc != n.PromiscMode { return brFound, fmt.Errorf("Bridge interface %s configured Promisc Mode %v doesn't match current state: %v ", intf.Name, n.PromiscMode, linkPromisc) } brFound.found = true brFound.Name = link.Attrs().Name brFound.ifIndex = link.Attrs().Index brFound.masterIndex = link.Attrs().MasterIndex return brFound, nil } func validateCniVethInterface(intf *current.Interface, brIf cniBridgeIf, contIf cniBridgeIf) (cniBridgeIf, error) { vethFound, link, err := validateInterface(*intf, false) if err != nil { return vethFound, err } _, isVeth := link.(*netlink.Veth) if !isVeth { // just skip it, it's not what CNI created return vethFound, nil } _, vethFound.peerIndex, err = ip.GetVethPeerIfindex(link.Attrs().Name) if err != nil { return vethFound, fmt.Errorf("Unable to obtain veth peer index for veth %s", link.Attrs().Name) } vethFound.ifIndex = link.Attrs().Index vethFound.masterIndex = link.Attrs().MasterIndex if vethFound.ifIndex != contIf.peerIndex { return vethFound, nil } if contIf.ifIndex != vethFound.peerIndex { return vethFound, nil } if vethFound.masterIndex != brIf.ifIndex { return vethFound, nil } if intf.Mac != "" { if intf.Mac != link.Attrs().HardwareAddr.String() { return vethFound, fmt.Errorf("Interface %s Mac doesn't match: %s not found", intf.Name, intf.Mac) } } vethFound.found = true vethFound.Name = link.Attrs().Name return vethFound, nil } func validateCniContainerInterface(intf current.Interface) (cniBridgeIf, error) { vethFound, link, err := validateInterface(intf, true) if err != nil { return vethFound, err } _, isVeth := link.(*netlink.Veth) if !isVeth { return vethFound, fmt.Errorf("Error: Container interface %s not of type veth", link.Attrs().Name) } _, vethFound.peerIndex, err = ip.GetVethPeerIfindex(link.Attrs().Name) if err != nil { return vethFound, fmt.Errorf("Unable to obtain veth peer index for veth %s", link.Attrs().Name) } vethFound.ifIndex = link.Attrs().Index if intf.Mac != "" { if intf.Mac != link.Attrs().HardwareAddr.String() { return vethFound, fmt.Errorf("Interface %s Mac %s doesn't match container Mac: %s", intf.Name, intf.Mac, link.Attrs().HardwareAddr) } } vethFound.found = true vethFound.Name = link.Attrs().Name return vethFound, nil } func cmdCheck(args *skel.CmdArgs) error { n, _, err := loadNetConf(args.StdinData) if err != nil { return err } netns, err := ns.GetNS(args.Netns) if err != nil { return fmt.Errorf("failed to open netns %q: %v", args.Netns, err) } defer netns.Close() // run the IPAM plugin and get back the config to apply err = ipam.ExecCheck(n.IPAM.Type, args.StdinData) if err != nil { return err } // Parse previous result. if n.NetConf.RawPrevResult == nil { return fmt.Errorf("Required prevResult missing") } if err := version.ParsePrevResult(&n.NetConf); err != nil { return err } result, err := current.NewResultFromResult(n.PrevResult) if err != nil { return err } var errLink error var contCNI, vethCNI cniBridgeIf var brMap, contMap current.Interface // Find interfaces for names whe know, CNI Bridge and container for _, intf := range result.Interfaces { if n.BrName == intf.Name { brMap = *intf continue } else if args.IfName == intf.Name { if args.Netns == intf.Sandbox { contMap = *intf continue } } } brCNI, err := validateCniBrInterface(brMap, n) if err != nil { return err } // The namespace must be the same as what was configured if args.Netns != contMap.Sandbox { return fmt.Errorf("Sandbox in prevResult %s doesn't match configured netns: %s", contMap.Sandbox, args.Netns) } // Check interface against values found in the container if err := netns.Do(func(_ ns.NetNS) error { contCNI, errLink = validateCniContainerInterface(contMap) if errLink != nil { return errLink } return nil }); err != nil { return err } // Now look for veth that is peer with container interface. // Anything else wasn't created by CNI, skip it for _, intf := range result.Interfaces { // Skip this result if name is the same as cni bridge // It's either the cni bridge we dealt with above, or something with the // same name in a different namespace. We just skip since it's not ours if brMap.Name == intf.Name { continue } // same here for container name if contMap.Name == intf.Name { continue } vethCNI, errLink = validateCniVethInterface(intf, brCNI, contCNI) if errLink != nil { return errLink } if vethCNI.found { // veth with container interface as peer and bridge as master found break } } if !brCNI.found { return fmt.Errorf("CNI created bridge %s in host namespace was not found", n.BrName) } if !contCNI.found { return fmt.Errorf("CNI created interface in container %s not found", args.IfName) } if !vethCNI.found { return fmt.Errorf("CNI veth created for bridge %s was not found", n.BrName) } // Check prevResults for ips, routes and dns against values found in the container if err := netns.Do(func(_ ns.NetNS) error { err = ip.ValidateExpectedInterfaceIPs(args.IfName, result.IPs) if err != nil { return err } err = ip.ValidateExpectedRoute(result.Routes) if err != nil { return err } return nil }); err != nil { return err } return nil } |
创建VETH对的过程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
// 从容器命名空间中调用此函数 // 自动将一端移动到“宿主机”命名空间(实际上是任何NS,不一定是真正的宿主机NS) func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (hostVeth net.Interface, containerVeth net.Interface, error) { return SetupVethWithName(contVethName, "", mtu, hostNS) } func SetupVethWithName(contVethName, hostVethName string, mtu int, hostNS ns.NetNS) (net.Interface, net.Interface, error) { // 宿主端 容器端 hostVethName, contVeth, err := makeVeth(contVethName, hostVethName, mtu) if err != nil { return net.Interface{}, net.Interface{}, err } // 启动容器端 if err = netlink.LinkSetUp(contVeth); err != nil { return net.Interface{}, net.Interface{}, fmt.Errorf("failed to set %q up: %v", contVethName, err) } hostVeth, err := netlink.LinkByName(hostVethName) if err != nil { return net.Interface{}, net.Interface{}, fmt.Errorf("failed to lookup %q: %v", hostVethName, err) } // 移动对端到宿主机命名空间 if err = netlink.LinkSetNsFd(hostVeth, int(hostNS.Fd())); err != nil { return net.Interface{}, net.Interface{}, fmt.Errorf("failed to move veth to host netns: %v", err) } // 在宿主机命名空间启动对端 err = hostNS.Do(func(_ ns.NetNS) error { hostVeth, err = netlink.LinkByName(hostVethName) if err != nil { return fmt.Errorf("failed to lookup %q in %q: %v", hostVethName, hostNS.Path(), err) } if err = netlink.LinkSetUp(hostVeth); err != nil { return fmt.Errorf("failed to set %q up: %v", hostVethName, err) } // we want to own the routes for this interface _, _ = sysctl.Sysctl(fmt.Sprintf("net/ipv6/conf/%s/accept_ra", hostVethName), "0") return nil }) if err != nil { return net.Interface{}, net.Interface{}, err } return ifaceFromNetlinkLink(hostVeth), ifaceFromNetlinkLink(contVeth), nil } func makeVeth(name, vethPeerName string, mtu int) (peerName string, veth netlink.Link, err error) { for i := 0; i < 10; i++ { if vethPeerName != "" { peerName = vethPeerName } else { // 给对端随机分配名称 peerName, err = RandomVethName() if err != nil { return } } veth, err = makeVethPair(name, peerName, mtu) switch { case err == nil: return case os.IsExist(err): if peerExists(peerName) && vethPeerName == "" { continue } err = fmt.Errorf("container veth name provided (%v) already exists", name) return default: err = fmt.Errorf("failed to make veth pair: %v", err) return } } // should really never be hit err = fmt.Errorf("failed to find a unique veth name") return } func makeVethPair(name, peer string, mtu int) (netlink.Link, error) { // 创建VETH结构 veth := &netlink.Veth{ LinkAttrs: netlink.LinkAttrs{ Name: name, Flags: net.FlagUp, MTU: mtu, }, // 对端(需要移动到宿主机的)名称,随机,例如veth99350820 PeerName: peer, } // 添加连接 if err := netlink.LinkAdd(veth); err != nil { return nil, err } // 创建连接后需要重新获取 // 因为索引、MAC等信息可能更新 veth2, err := netlink.LinkByName(name) if err != nil { netlink.LinkDel(veth) // 出错则删除设备并清理 return nil, err } return veth2, nil } |
使用配置:
1 2 3 4 5 6 7 8 |
{ "cniVersion": "0.3.1", "name": "testConfig", "type": "bridge", "bridge": "bridge0", "vlan": 100, "ipam": {} } |
获得应答:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
{ "cniVersion": "0.3.1", "interfaces": [ { "name": "bridge0", "mac": "ce:d7:70:c7:97:08" }, { "name": "veth99350820", "mac": "ce:d7:70:c7:97:08" }, { "name": "eth0", "mac": "ce:87:b0:d4:94:c3", "sandbox": "/var/run/netns/cnitest-cdf173b0-fa2a-66ab-e5d6-bba23a0708f5" } ], "dns": {} } |
产生的网络拓扑如下:
Leave a Reply