cni设计
参考: https://github.com/containernetworking/cni/blob/main/SPEC.md
1. 插件式使用的cni
术语理解:
container: 在cni这里对应linux network ns
network: 指一组endpoint,可以用于获取到唯一的地址
runtime: 一个负责执行CNI plugin的程序
plugin: 负责配置网络
这个文档会仅对runtime 和 plugin进行说明
第一部分: 网络配置格式
cniVersion: 版本,版本号越大支持的特性越丰富
name: 网络名
disableCheck: 对应cni的check (Add|Del|check)
plugins: cni list
plugin 配置对象
type: 对应cni二进制的名字,比如macvlan,ipvlan
capabilities: 稍后补充
ipMasq: 通过host上的一个ip来伪装整个网络,所有pod用的该网段的pod ip都会使用该host ip进行地址伪装(nat)
ipam: 地址管理字典,其内的type字段即是ipam plugin
dns: 一个字典,包括,nameservers,domain,search,options
{
"cniVersion": "1.0.0",
"name": "dbnet",
"plugins": [
{
"type": "bridge",
// plugin specific parameters
"bridge": "cni0",
"keyA": ["some more", "plugin specific", "configuration"],
"ipam": {
"type": "host-local",
// ipam specific
"subnet": "10.1.0.0/16",
"gateway": "10.1.0.1",
"routes": [
{"dst": "0.0.0.0/0"}
]
},
"dns": {
"nameservers": [ "10.1.0.1" ]
}
},
{
"type": "tuning",
"capabilities": {
"mac": true
},
"sysctl": {
"net.core.somaxconn": "500"
}
},
{
"type": "portmap",
"capabilities": {"portMappings": true}
}
]
}
第二部分, 执行协议
cni protocol协议基于容器运行时调用cni(二进制),cni定义了cni二进制脚本和容器运行时交互的协议。
plugin分两种。
"Interface" plugins: 常见的cni macvlan ipvlan等
"Chained" plugins: 基于cni配置好的接口,做qos,安全组等配置,比如terway 的ipvlan和之后的cilium链式调用
容器运行时,通过将配置和环境变量以stdin的形式传递给cni插件。cni插件配置完容器接口以及网络之后,基于stdout 反馈成功。 如果有错误,基于strerr反馈。
stdin的配置和stdout的结果都是JSON格式。
成功是状态码 必须是0
失败时 非0
容器运行时是在root network ns中执行cni plugin的。
传递给cni的json包括:
CNI_COMMAND: ADD, DEL, CHECK, or VERSION
CNI_CONTAINERID: 容器id
CNI_NETNS: 容器所在网络命名空间 /run/netns/[nsname]
CNI_IFNAME: 容器内部的网卡
CNI_ARGS: 额外的参数,格式为 "FOO=BAR;ABC=123"
CNI_PATH: CNI插件所在路径
cni 的操作
ADD 将容器连接到网络,或者执行变更
- 在CNI_NETNS内部, 基于CNI_IFNAME 创建网卡
- 或者在CNI_NETNS内部, 基于CNI_IFNAME 调整接口的配置
如果ADD成功,那么返回的json 也就是 prevResult 对象会包括这些内容: https://github.com/containernetworking/cni/blob/main/SPEC.md#Success
cniVersion:
容器网卡: name, mac, samdbox(CNI_NETNS)
ips: ipcidr 192.168.1.3/24, gw, 网卡index
路由: dst cidr, gw
dns: nameservers domain search options
如果容器内有同样的接口,肯定会报错,而且,容器运行时不会连续执行ADD两次(中间没有删除动作),仅执行一次。
输入:
容器运行时会提供一个json 对象作为cni的标输入:
必须包含的字段:
- CNI_COMMAND
- CNI_CONTAINERID
- CNI_NETNS
- CNI_IFNAME
可选字段:
CNI_ARGS
CNI_PATH
DEL是add的反操作,暂不写
CHECK 用于检查和确认,暂不写
ipam 代理插件
Delegated plugins (IPAM)
ipam 插件必须反馈一个缩略的的成功对象的结构体。 比如没有接口数组以及接口ip等。
ipam插件式cni设计的一部分,ipam和cni二级制协调工作,才能完成网络配置
第三部分, 网络配置的执行
容器运行时会在容器内部执行 add, del, check 等网络操作。
- 网络配置如何转化
- 转化后的网络配置如何提供给插件
网络配置的操作统一叫做 attachment, 容器id 和 接口id, 是attachment的唯一标识
(操作)周期 和 顺序
- 在调用任何插件(cni ipam)之前,容器运行时必须已经创建了一个新的网络命名空间。
- 对于同一个容器,容器运行时不能调用多个并行的操作。 多个容器的操作可以并行。
- 不同的容器,插件可以并行执行,必要的话,需要实现一定的锁机制,必须ipam 的地址分配。
- 容器运行时必须保证,add 和 delete的先后顺序, add 结束,delete才能执行,即使add失败,del必须成功执行。
- delete保证幂等,无论delete多少次,结果一致。
- add 和 delete 之间的时间段, 网络配置不能发生变化。
- 在不同的attachments之间,网络配置不能发生变化。
- 容器运行时负责ns的清理。
Attachment的参数:
- 容器id
- ns
- 容器网卡名
- 原生参数, 额外参数,键值对格式,在CNI_ARGS 里面
- Capability Arguments, 键值对,json 格式: https://github.com/containernetworking/cni/blob/main/CONVENTIONS.md
"capabilities": {"portMappings": true}
"runtimeConfig": {
"portMappings": [
{"hostPort": 8080, "containerPort": 80, "protocol": "tcp"}
]
}
添加一个attachment
# cat /etc/cni/net.d/00-multus.conf | jq
{
"cniVersion": "0.4.0",
"name": "multus-cni-network",
"type": "multus",
"capabilities": {
"portMappings": true
},
"kubeconfig": "/etc/cni/net.d/multus.d/multus.kubeconfig",
"delegates": [
{
"name": "kube-ovn",
"cniVersion": "0.3.1",
"plugins": [
{
"type": "kube-ovn",
"server_socket": "/run/openvswitch/kube-ovn-daemon.sock"
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
}
]
}
]
}
# 比如当前的k8s集群环境
# plugins包含两部分 cni是multus,ipam是kube-ovn-daemon
- 查看type 字段,不存在,则失败,报错
- 基于plugin 配置,去请求配置,包括如下参数
如果这个plugin在list中的第一位, 不需要提供之前的结果,应该指的是默认cni。
其他的plugins, 之前的结果就是上一个plugin处理过的输出。
这里说明,主plugin和其他plugin是有一定的顺序链式调用关系的。
-
执行plugin 二进制(比如macvlan cni), 基于CNI_COMMAND=ADD 参数,二进制可以通过环境变量读入参数,stdin也有包含一些配置项(ip mac dns 网关 svc localdns等)。
-
一旦plugin 执行出错,会立刻停止并失败,返回错误。
容器运行时,必须存储最后一个plugin返回的结果,并持久化存储(pod的状态),用于做pod状态检查以及是否删除。
Deleting an attachment: 是add的反向操作,暂不写
从 plugin 配置中,获取(待)执行配置项
网络配置的格式: 就是一个list,包含所有需要执行的plugin配置内容。
必须被转化为单个plugin能够理解的格式,因为即使是多网卡场景,也要一个一个的执行。
单个plugin调用时需要的配置也是一个json。
容器运行时必须将以下配置项传递给cni二进制
- cniVersion: cni 版本
- name: 取自网络配置中的name字段
- runtimeConfig: json 对象, plugin中的capabilities 字段
- prevResult: 一个json对象,上一个运行过的plugin(add/delete/check)返回的结果
plugin 中定义的 capabilities会被容器运行时移除。 其他field都毫无更改的直接传递。
runtimeConfig
然而 当CNI_ARGS 被传递给所有plugins后,没有任何指示表示这些信息会被消费。
所以,Capability需要在plugins字段中明确定义。 那么 容器运行时,就可以基于此来确定是否一个给定的网络配置支持某些特定的capability(能力)。
{
"type": "myPlugin",
"capabilities": {
"portMappings": true // 表示支持端口映射的能力
}
}
// 将 capabilities 格式化为 runtimeConfig
{
"type": "myPlugin",
"runtimeConfig": {
"portMappings": [ { "hostPort": 8080, "containerPort": 80, "protocol": "tcp" } ]
}
...
}
runtimeConfig 的内容来自于 网络配置中的capabilities域
第四部分 插件代理(Plugin Delegation)
有一些操作,无论出于何种原因,都不能合理地实现为一个离散的链式插件。
更确切的说,一个CNI plugin 希望能代理别的插件执行一些功能。 一个最常用的例子就是代理执行ip 地址分配(ipam)
作为网络配置的一部分,CNI 插件需要能够分配并维护容器内网卡的ip地址,也包括配置必要的路由。
这让CNI插件具有极大的灵活性,但也变得更重(ipam 路由 dns 这些东西的维护)。
很多CNI插件需要的ipam的代码几乎都是类似的,比如dhcp host-local。
所以, 一个CNI 插件需要设计为可以用来作为其他plugin的ipam代理。
为了让CNI更轻便,而且让IPAM 和 cni (二进制)保持正交关系(低耦合)。
正交设计原则
消除重复
分离关注点
缩小依赖范围
向稳定的方向依赖
ipam 被设计为一个第三方的插件,为了保证ipam被代理调用,同时设计了一个协议,用于指导代理(ipam)功能的实现。
然而这不是容器运行时的职责,而是CNI plugin的职责: 在执行的过程中,适当的时间点,调用IPAM插件。
ipam 插件必须定义了ip,子网,网关,路由。 并且返回这些信息给“主” 插件去应用(把这些参数在调用cni二进制的时候给过去)。
ipam插件也有可能通过协议(dhcp)来获取这些信息。 或者数据存储在本地文件系统中。 或者网络配置的ipam部分。
被代理的插件协议(Delegated Plugin protocol)
和cni 插件一样,被代理的(ipam)插件也是通过调用一个可执行的(二进制)。 这些插件也在预先指定好的目录下,
# ls -l /opt/cni/bin/
total 177440
-rwxr-xr-x. 1 root root 4151672 Feb 5 2021 bandwidth # 带宽限制
-rwxr-xr-x. 1 root root 4536104 Feb 5 2021 bridge
-rwxr-xr-x. 1 root root 10270090 Feb 5 2021 dhcp # ipam
-rwxr-xr-x. 1 root root 4767801 Feb 5 2021 firewall # 防火墙限制
-rwxr-xr-x. 1 root root 3357992 Feb 5 2021 flannel
-rwxr-xr-x. 1 root root 4144106 Feb 5 2021 host-device
-rwxr-xr-x. 1 root root 3565330 Feb 5 2021 host-local # ipam
-rwxr-xr-x. 1 root root 4288339 Feb 5 2021 ipvlan
-rwxr-xr-x. 1 root root 65658880 Apr 10 03:51 kube-ovn
-rwxr-xr-x. 1 root root 3566204 Apr 10 03:51 loopback
-rwxr-xr-x. 1 root root 4497003 Apr 10 03:51 macvlan
-rwxr-xr-x. 1 root root 41849770 Apr 10 03:48 multus
-rwxr-xr-x. 1 root root 3979034 Apr 10 03:51 portmap # 端口映射
-rwxr-xr-x. 1 root root 4467317 Feb 5 2021 ptp
-rwxr-xr-x. 1 root root 3701138 Feb 5 2021 sbr
-rwxr-xr-x. 1 root root 3153330 Feb 5 2021 static
-rwxr-xr-x. 1 root root 3668289 Feb 5 2021 tuning
-rwxr-xr-x. 1 root root 4287972 Feb 5 2021 vlan
-rwxr-xr-x. 1 root root 3759977 Feb 5 2021 vrf
CNI_PATH: 用于指定CNI的路径
所有被代理的cni插件收到的环境变量都是一样的,就像cni插件一样,通过stdin收到网络配置,基于stdout传出配置。
被代理的插件(下层的)提供完整的网络配置给到(上层的)插件,(插件间按照主次顺序调用)
被代理的插件执行流程
当一个插件执行一个被代理的插件时,它应该:
在CNI_PATH(环境变量)对应目录下查找二进制对象
根据插件收到的配置和环境变量执行找到的二进制对象(执行时传给配置和环境变量)
确保 被代理的插件的stderr被传递给调用者的stderr对象
ADD 场景中,如果被代理的插件执行失败了,那么在返回错误前需要执行该插件的del操作。
同样的,DEL | check 上层插件检测失败,要将stderr返回给下层。
第五部分,返回值类型
只有3种类型
Success (or Abbreviated Success)
Error
_Version
5.1 Success
所有插件的请求配置内容中都会有一个 prevResult, prevResult 作为插件自身的输出。
任何插件都可能会对 prevResult 进行修改。 如果不修改,直接反射(传递)即可。
ADD 操作成功后,所有插件(链式执行后)必须返回一个json对象:
cniVersion:
interfaces: 网卡名, mac, ns
ips: attachment 配置好的ipcidr, 网关, 网卡index
routes: 路由 dst cidr,gw
dns: nameservers domain search options
ipam (被代理的插件)
被代理的插件可以忽略 相关的配置项。
ipam 必须返回一个缩略的 Success 成功对象,可能不会包含 interfaces 数组,以及ips 对象内部的interface。
错误对象暂时不了解
版本
插件必须返回cniVerison, 以及 supportedVersions
{
"cniVersion": "1.0.0",
"supportedVersions": [ "0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0", "1.0.0" ]
}
示例
- CNI_COMMAND=ADD, 把如下参数传递给 bridge cni二级制 插件
{
"cniVersion": "1.0.0",
"name": "dbnet", // 网络名
"type": "bridge", // cni 二进制的名字
"bridge": "cni0", // 参数 网桥名
"keyA": ["some more", "plugin specific", "configuration"], // 额外参数
"ipam": { // 被代理调用的ipam 插件
"type": "host-local",
"subnet": "10.1.0.0/16",
"gateway": "10.1.0.1"
},
"dns": { // 被代理调用的 dns 插件 ?
"nameservers": [ "10.1.0.1" ]
}
}
接下来 主 cni (比如multus),会调用ipam(基于ipam内的参数,执行调用host-local二进制,将subnet gw属性基于stdin传递给该二进制)
host-local 会返回如下结果
{
"ips": [
{
"address": "10.1.0.5/16", // 分配出ip
"gateway": "10.1.0.1" // 返回网关
}
],
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"dns": {
"nameservers": [ "10.1.0.1" ]
}
}
然后 bridge 二进制cni文件,基于ipam的返回结果,配置容器的网络,然后返回如下结果
{
"ips": [
{
"address": "10.1.0.5/16",
"gateway": "10.1.0.1",
"interface": 2
}
],
"routes": [
{
"dst": "0.0.0.0/0"
}
], // 以上是ipam的返回内容,保持不变
"interfaces": [ //以下是cni添加的内容
{
"name": "cni0", // 网桥名
"mac": "00:11:22:33:44:55" // 网桥的mac
},
{
"name": "veth3243", // veth-pair 接在网桥的一段
"mac": "55:44:33:22:11:11" // 其mac
},
{
"name": "eth0", // 容器ns 内部的eth0 网卡,veth-pair的另一端
"mac": "99:88:77:66:55:44", // mac
"sandbox": "/var/run/netns/blue" // ns 路径
}
],
"dns": {
"nameservers": [ "10.1.0.1" ]
}
}
这个结果就是 bridge cni操作后的 prevResult。
第一个插件,一般是cni 二进制,它的prevResult是空的,但是它的(下一个|上层)插件会基于第一个插件的result作为prevResult。
- 基于 tuning (二进制)插件, 基于CNI_COMMAND=ADD, 调用 tuning的 add函数,基于 prevResult, 结合mac的capability (特性),stdin 收到的配置请求体如下
{
"cniVersion": "1.0.0",
"name": "dbnet",
"type": "tuning", // 二进制插件
"sysctl": {
"net.core.somaxconn": "500"
},
"runtimeConfig": { // 特性, 将eth0的mac调整为该值
"mac": "00:11:22:33:44:66"
},
"prevResult": { // 下层的返回
"ips": [
{
"address": "10.1.0.5/16",
"gateway": "10.1.0.1",
"interface": 2
}
],
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"interfaces": [
{
"name": "cni0",
"mac": "00:11:22:33:44:55"
},
{
"name": "veth3243",
"mac": "55:44:33:22:11:11"
},
{
"name": "eth0",
"mac": "99:88:77:66:55:44",
"sandbox": "/var/run/netns/blue"
}
],
"dns": {
"nameservers": [ "10.1.0.1" ]
}
}
}
tuning 成功执行后的返回结果,可以看到eth0 mac已发生变化
{
"ips": [
{
"address": "10.1.0.5/16",
"gateway": "10.1.0.1",
"interface": 2
}
],
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"interfaces": [
{
"name": "cni0",
"mac": "00:11:22:33:44:55"
},
{
"name": "veth3243",
"mac": "55:44:33:22:11:11"
},
{
"name": "eth0",
"mac": "00:11:22:33:44:66", // 已变化
"sandbox": "/var/run/netns/blue"
}
],
"dns": {
"nameservers": [ "10.1.0.1" ]
}
}
tuning返回的prevResult
- 最后 调用 portmap 插件,CNI_COMMAND=ADD 对应 portmap add函数,使用的 prevResult如上,
而 tuning 执行完毕后,如下
{
"cniVersion": "1.0.0",
"name": "dbnet",
"type": "portmap", // 增加了该部分
"runtimeConfig": {
"portMappings" : [
{ "hostPort": 8080, "containerPort": 80, "protocol": "tcp" }
]
},
"prevResult": {// 下层的返回
"ips": [
{
"address": "10.1.0.5/16",
"gateway": "10.1.0.1",
"interface": 2
}
],
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"interfaces": [
{
"name": "cni0",
"mac": "00:11:22:33:44:55"
},
{
"name": "veth3243",
"mac": "55:44:33:22:11:11"
},
{
"name": "eth0",
"mac": "00:11:22:33:44:66",
"sandbox": "/var/run/netns/blue"
}
],
"dns": {
"nameservers": [ "10.1.0.1" ]
}
}
}
DELETE正好是add 的逆操作,所以最上层的插件先执行,执行的参数就是上面ADD最后返回的结果。
网友评论