本文对CNI标准v1.0.0版本进行了翻译,翻译出来感觉和自己看的时候还是有点不一样,毕竟直接理解跟翻译成文本还是有一定的区别,所以如果有什么译错的地方可以直接联系修改。
标准原文链接:https://github.com/containernetworking/cni/blob/spec-v1.0.0/SPEC.md
个人博客链接:https://www.gogo-dev.com/index.php/2022/03/26/cni01/
本文为Linux平台上的容器应用提供了一个基于插件的通用网络解决方案,即容器网络接口或CNI。
为了达到本文的目的,我们明确地定义了如下3个(个人标注:估计是笔误,4个)术语:
CNI标准定义了:
CNI为系统管理员定义了网络配置格式。它包含了给容器运行时和所调用插件使用的指令。在插件执行的时候,由容器运行时按照标准格式翻译和格式化配置,然后传递给插件。
一般来说,这个网络配置格式是比较固定的。通常我们可以认为它是有落盘到硬盘上的,虽然CNI标准并没有明确要求这一点(个人标注:比如kubernetes平台中主机目录/etc/cni/net.d/下放的配置文件)。
一个网络配置是包含有如下关键词的JSON对象:
插件配置对象可能会包含这里定义的配置项以外的配置项。如章节3里定义的所述,运行时必须不加改变的传递这些配置项给插件。
这些关键词由运行时在运行的时候生成,因此不能在配置中使用(个人标注:因为运行时本来就会生成这些配置项,所以插件不能在配置文件中再重新定义这些配置项从而导致冲突)。
这些关键词没有被协议使用,但是对于插件有着标准的含义。插件应该遵循这些关键词的本意来使用这些关键词。
插件可以自定义额外的它们能接受的关键词,同时在接收到未知的关键词时可以响应错误。运行时在解析和格式化插件配置对象的时候,必须保留自己未知的关键词配置。(个人标注:这里即允许插件支持定制化参数,用户通过在网络配置文件中指定从而进行传参赋值)
{
"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}
}
]
}
(个人标注:上面就给出了网络配置的示例,plugins配置项就是插件配置对象的示例,其中配置了多个插件,每个插件有自己的配置;比如bridge插件就配置的比较全面,包括配置了ipam和dns等,bridge、keyA就是bridge插件的定制参数)
CNI标准协议是基于那些被容器运行时调用的插件二进制文件的。CNI定义了插件二进制和容器运行时之间的协议。
一个CNI插件负责通过某些方式为容器配置网络接口。插件可以分为2类:
参数(Parameters)定义了运行时特定的设置,然而配置(configuration)则是对于任意给定网络都是一致的。
容器运行时必须在运行时的网络域中调用插件。(在大部分情况下,这都是根网络命名空间/ dom0)(个人标注:即容器运行时都是在宿主机网络空间下调用插件二进制的)
协议参数是通过OS环境变量传递给插件的。
在执行成功的时候,插件必须返回值0;而执行出错的时候返回非0值。如果插件遇到一个错误,那它应该输出一个error结构的结果(见下文)。
CNI定义了4个操作:ADD、DEL、CHECK和VERSION(个人标注:早期是没有CHECK操作的,应该是最近版本引入的)。这些通过环境变量CNI_COMMAND传递给插件。
如果在容器内已经存在了同名的网卡,则插件必须返回错误。
容器运行时不能为相同的CNI_CONTAINERID、CNI_IFNAME组连续调用2次ADD操作(在中间没有调用DEL的情况下)。这意味着一个容器只能通过不同的网络接口名称接入一个特定的网络。
运行时会在标准输入stdin中提供JSON序列化的插件配置。
必须传递的环境变量:
CNI_COMMAND
CNI_CONTAINERID
CNI_NETNS
CNI_IFNAME
可选的环境变量:
CNI_ARGS
CNI_PATH
一个CNI插件在接收到DEL命令的时候,应该
对于同一对CNI_CONTAINERID、CNI_IFNAME,即使是在请求中的网卡或者其他已添加的配置都已经不存在的时候,插件也必须可以接受多次重复调用DEL操作,然后返回成功。
运行时会在标准输入stdin中提供JSON序列化的插件配置。
必须传递的环境变量:
CNI_COMMAND
CNI_CONTAINERID
CNI_IFNAME
可选的环境变量:
CNI_NETNS
CNI_ARGS
CNI_PATH
CHECK提供给容器运行时探测一个现有容器的网络状态。
运行时会在标准输入stdin中提供JSON序列化的插件配置。
必须传递的环境变量:
CNI_COMMAND
CNI_CONTAINERID
CNI_NETNS
CNI_IFNAME
可选的环境变量:
CNI_ARGS
CNI_PATH
除了CNI_PATH,所有的参数都必须和调用ADD操作时的参数保持一致。
插件应该通过标准输出stdout返回一个版本结果结构的输出(见下文)。
JOSN序列化的对象,包含以下关键词:
cniVersion:当前使用的协议版本
必须传递的环境变量:
CNI_COMMAND
本章节描述了容器运行时是如何转义网络配置(章节1中定义的网络配置),然后照此调用插件。容器运行时可能会在容器内add、delete或者check一个网络配置。对应的,运行时就是调用一系列插件的ADD、DELETE或者CHECK操作。本章节也定义了网络配置是如何格式化并传递给插件。
容器内一个网络配置的操作被称之为attachment。一个attachment可以用CNI_CONTAINERID、CNI_IFNAME组唯一识别。
因为在attachment之间网络配置不应该发生改变,所以有些确定的参数是在attachment之前由运行时提供的。即以下的参数:
对于网络配置中plugins参数中的每个配置,
删除一个网络attachment基本和add操作相同,但是也有如下几点不同:
运行时可以向每个插件查询确认给定的attachment仍然是正常的。运行时调用的attachment参数必须和之前add操作给出的参数一致。
检查动作和add动作也基本相似,除了,
网络配置格式(要调用的插件配置列表)必须转换成插件可以识别的格式(单个插件配置)。本节描述了这个转换过程。
单个插件调用的执行参数也是JSON格式的。它由插件配置组成,通常是不会改变了,除了一些规定的添加项和移除项。
以下字段必须在调用的时候由运行时添加到执行配置中:
cniVersion:从网络配置的cniVersion字段获取。
name:从网络配置的name字段获取。
runtimeConfig:JSON格式的对象,由插件所提供特性和运行时所请求特性的组合。
prevResult:JSON格式的对象,由之前插件返回的结果组成。这里的"之前插件"是由特定的操作定义的(add、delete或者是check)。
以下字段必须由运行时移除:
capabilities
其它字段则应该不加修改的传递。
因为所有插件都提供了CNI_ARGS,但是并没有指明它们都必须被处理,因此Capability参数需要在配置中明确声明。运行时因此可以确定一个给定的网络配置是否支持某个特定capability。Capability并没有在本标准中定义,相反的,它们在convention(https://github.com/containernetworking/cni/blob/spec-v1.0.0/CONVENTIONS.md)中描述。
在章节1中定义可知,插件配置包含一个可选的参数,capabilities。下面的例子展示了一个插件支持了portMapping的特性:
{
"type": "myPlugin",
"capabilities": {
"portMappings": true
}
}
runtimeConfig参数是从网络配置的capabilities参数中提出的,然后运行时生成了capability参数。确切的说,插件支持的任何capability以及运行时提供的特性都应该填入runtimeConfig中。
因此,上面的例子最后就会生成如下的配置作为执行参数的一部分传递给插件:
{
"type": "myPlugin",
"runtimeConfig": {
"portMappings": [ { "hostPort": 8080, "containerPort": 80, "protocol": "tcp" } ]
}
...
}
有些操作鉴于某些原因是无法由分离的插件链来合理实现的。相反,一个CNI插件可能会期望委托一些功能给其它插件来完成。比如一个常见的例子就是IP地址的分配。
作为插件操作的一部分,它可能被期望能够为网络接口分配(和管理)一个IP地址,然后配置和这个接口相关的路由。这会使插件变得非常灵活,但也给它带来了巨大的负担。很多的CNI插件不得不引入相同的代码来支持用户期望的不同IP管理机制(比如dhcp,host-local)。因此一个CNI插件可能就会选择将IP管理委托给另一个插件。
为了减轻插件的负担,以及使得IP管理策略和CNI插件类型是正交的(个人标注:这里的正交不知道该怎么理解),我们定义了另一种类型的插件,也就是IP地址管理插件(IPAM插件)。同时定义了插件委托功能给其它插件直接的交互协议。
在合适的时机调用IPAM插件是CNI插件的责任,而不是容器运行时的。IPAM插件必须确定网卡所使用的IP、子网、网关和路由,然后将这些信息返回给调用它的主插件。IPAM插件还可能通过某些协议比如dhcp获取信息,然后将数据保存在本地文件系统中,或者网络配置文件的ipam段里,等等。
和CNI插件一样,被委托插件也是通过执行一个可执行文件来调用的。而这个可执行文件的搜索路径是预设的一个路径列表,即通过CNI_PATH传递给CNI插件的值。那些传递给CNI插件的环境变量必须原封不动的也传递给被委托插件。和CNI插件一样,被委托插件也是从标准输入stdin获取网络配置,并将结果输出到标准输出stdout。
上层调用插件必须把它接收到的完整网络配置也传递给被委托插件。换句话说,在IPAM的场景中,应该传递整个网络配置,而不仅仅是ipam这个段。
返回值为0则表示执行成功,同时标准输出stdout会输出一个Success类型结果。
当一个插件要调用一个被委托插件,它必须:
如果是ADD操作时,如果一个被委托插件返回了错误,则上层插件必须在返回错误前调用DEL操作。
插件可以返回以下三种结果类型的任意一种:
如果传递给插件的请求配置中有包含preResult的,则必须也将preResult作为结果返回,而且插件可能会对这个preResult做修改。如果插件对其没有改动(这个会体现在Success结果中),则它必须返回一个和传入的prevResult相同的结果。
对于一个成功的ADD操作,插件必须返回一个包含如下keys的JSON对象:
被委托插件可能会省去不相关的部分。
被委托的IPAM插件必须返回一个简短的Success对象。特别的,它不包含interfaces列表,也不包含ips中的interface元素。
当插件遇到错误的时候,它们应该返回包含如下keys的JSON对象:
{
"cniVersion": "1.0.0",
"code": 7,
"msg": "Invalid Configuration",
"details": "Network 192.168.0.0/31 too small to allocate from."
}
错误码0-99被保留用于常见的错误。100以上的值可以由插件用于指示其自定义的错误。
错误码 | 错误描述 |
---|---|
1 | CNI版本不兼容 |
2 | 网络配置中有不支持的字段。错误描述信息中必须包含不支持字段的key和键值。 |
3 | 容器未知或者不存在。该错误意味着运行时并不需要进行任何容器的网络清理(比如,为容器调用DEL操作)。 |
4 | 必配环境变量项不合法,比如CNI_COMMAN、CNI_CONTAINERID等。 |
5 | I/O错误。比如,从stdin读取网络配置失败。 |
6 | 序列化内容错误。比如,从字节流中序列化网络配置失败,或者是从字符串中读取版本号失败。 |
7 | 不合法的网络配置。如果某些网络配置校验失败,就将会返回该错误。 |
11 | 稍后重试。如果插件发现有些短暂的条件需要清理,那它可以使用该错误码来通知运行时稍后重试相关操作。 |
另外,标准错误输出stderr将被用来输出非结构化的结果,比如日志。
对于VERSION操作,插件必须返回包含如下keys的JSON对象:
{
"cniVersion": "1.0.0",
"supportedVersions": [ "0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0", "1.0.0" ]
}
我们假设使用章节1中的网络配置。对于该attachment,运行时生成portmap和mac特性参数,同时生成一般参数"argA=foo"。以下示例使用CNI_IFNAME=eth0。
容器运行时对于add操作将会做如下几个步骤。
1.使用如下的JSON对象,CNI_COMMAND=ADD,调用bridge插件:
{
"cniVersion": "1.0.0",
"name": "dbnet",
"type": "bridge",
"bridge": "cni0",
"keyA": ["some more", "plugin specific", "configuration"],
"ipam": {
"type": "host-local",
"subnet": "10.1.0.0/16",
"gateway": "10.1.0.1"
},
"dns": {
"nameservers": [ "10.1.0.1" ]
}
}
而bridge插件将其IPAM功能委托给了host-local插件,因此将会调用host-local二进制。调用host-local时会将其收到的参数原封不动地传递给它,CNI_COMMAND置为ADD。
假设host-local插件返回如下结果:
{
"ips": [
{
"address": "10.1.0.5/16",
"gateway": "10.1.0.1"
}
],
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"dns": {
"nameservers": [ "10.1.0.1" ]
}
}
然后bridge插件将会根据返回的IPAM配置而配置网卡,同时返回如下结果给运行时:
{
"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" ]
}
}
2.然后,CNI_COMMAND置为ADD,调用tuning插件。要注意的是传入了prevResult参数,以及mac特性参数。传入的网络配置为:
{
"cniVersion": "1.0.0",
"name": "dbnet",
"type": "tuning",
"sysctl": {
"net.core.somaxconn": "500"
},
"runtimeConfig": {
"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插件返回了如下结果。可以看到其中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" ]
}
}
3.最后,CNI_COMMAN置为ADD,调用portmap插件。要注意的是preResult是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" ]
}
}
}
portmap插件的返回结果将会和bridge插件返回的结果一样,因为插件并没有做什么会影响返回结果的改动(比如,它只是创建了iptables规则而已)。
在上述ADD操作的基础上,容器运行时可能会执行如下步骤来完成Check动作:
1.首先根据如下请求配置调用bridge插件,包括从Add操作返回的最终JSON响应作为preResult,包括已经被更改的mac地址,CNI_COMMAND置为CHECK,
{
"cniVersion": "1.0.0",
"name": "dbnet",
"type": "bridge",
"bridge": "cni0",
"keyA": ["some more", "plugin specific", "configuration"],
"ipam": {
"type": "host-local",
"subnet": "10.1.0.0/16",
"gateway": "10.1.0.1"
},
"dns": {
"nameservers": [ "10.1.0.1" ]
},
"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" ]
}
}
}
然后bridge插件因为把IPAM委托给了host-local插件,因此它会调用host-local插件的CNI_COMMAND=CHECK。host-local插件直接返回。
假设bridge插件检查通过了,也没有在标准输出stdout中产生任何输出,而是直接返回返回值0。
2.然后运行时调用tuning插件,传入如下请求配置:
{
"cniVersion": "1.0.0",
"name": "dbnet",
"type": "tuning",
"sysctl": {
"net.core.somaxconn": "500"
},
"runtimeConfig": {
"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": "00:11:22:33:44:66",
"sandbox": "/var/run/netns/blue"
}
],
"dns": {
"nameservers": [ "10.1.0.1" ]
}
}
}
同样的,tuning插件也是成功返回。
3.最后,调用portmap插件,入参如下:
{
"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" ]
}
}
}
给定和上文一样的网络配置JSON列表,容器运行时也将会执行如下步骤来完成Delete操作。需要注意的是插件的调用将会是和Add、Check动作相反。
1.首先,根据如下请求配置调用portmap插件,CNI_COMMAND置为DEL:
{
"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" ]
}
}
}
2.然后,根据如下请求配置调用tuning插件,CNI_COMMAND置为DEL:
{
"cniVersion": "1.0.0",
"name": "dbnet",
"type": "tuning",
"sysctl": {
"net.core.somaxconn": "500"
},
"runtimeConfig": {
"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": "00:11:22:33:44:66",
"sandbox": "/var/run/netns/blue"
}
],
"dns": {
"nameservers": [ "10.1.0.1" ]
}
}
}
3.最后,调用bridge插件:
{
"cniVersion": "1.0.0",
"name": "dbnet",
"type": "bridge",
"bridge": "cni0",
"keyA": ["some more", "plugin specific", "configuration"],
"ipam": {
"type": "host-local",
"subnet": "10.1.0.0/16",
"gateway": "10.1.0.1"
},
"dns": {
"nameservers": [ "10.1.0.1" ]
},
"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" ]
}
}
}
在bridge插件返回前,它将会调用被委托插件host-local,传入的CNI_COMMAND置为DEL。