Overview
本文提出了一个通用的基于插件的Linux容器网络解决方案,容器网络接口,CNI。它脱胎于旨在满足大多数rtk网络设计的rtk Networking Proposal。
首先,我们对如下两个名词进行具体的定义:
- container可以认为是与Linux network namespace是同义的。而一个network namespace具体对应什么,则与具体的容器运行时有关:例如,在rtk中,每个pod运行在一个单独的network namespace中。但是在docker中,network namespace存在于每个独立的Docker容器中
- network代表了一组可以独立寻址并且可以互相交互的实体。这些实体既可以是一个单独的容器(如上所述),一台机器,或者其他什么网络设备(例如,一台路由器)。container可以加入一个或多个network,也可以从一个或多个network中移除。
本文旨在说明容器运行时和插件之间的接口。同时,可能还有些众所周知的字段,runtime也想传递给底层的插件,不过这些内容并不在本文中进行描述。
General consideration
首先容器运行时需要为container新建一个network namespace。之后,它需要决定该container属于哪些network,对于每个network还需要确定对应执行哪些插件。network configuration是以JSON格式存在的,很容易被存储在文件中。配置中需要包含一些必须的字段,例如,"name",“type”以及相应的插件必须的字段。同时,network configuration允许其中的字段在不同的调用间发生改变。因此,存在一个可选的"args"字段用于存放异变的信息。最后,容器运行时通过顺序地调用相应的插件来创建相应的network。当container的生命周期结束时,运行时再以相反的顺序调用插件,将它们从networks中移除。
CNI Plugin
Overview
每个CNI插件都是以一个能被容器管理系统(比如,rkt或者Docker)调用的可执行文件的形式存在的。
CNI插件负责将一个network interface插入container network namespace(比如,veth pair的其中一端)并且在宿主机中做一些必要的配置(例如将veth的另一端加入bridge中)。接着通过调用适当的IPAM插件,将IP赋给interface并且设置路由。
Parameters
CNI插件支持如下三种操作:
- 将container加入network(Add):
- Parameters:
- Version. 调用者使用的CNI 配置的版本信息
- Container ID. 这个字段是可选的,但是建议使用,在容器活着的时候要求该字段全局唯一的。比如,存在IPAM的环境可能会要求每个container都分配一个独立的ID,这样每一个IP的分配都能和一个特定的容器相关联。例如,在appc implementations中,container ID其实就是pod ID
- Network namespace path. 这个字段表示要加入的network namespace的路径。例如,/proc/[pid]/ns/net或者对于该目录的bind-mount/link。
- Network configuration. 这是一个JSON文件用于描述container可以加入的network,具体内容在下文中描述
- Extra arguments. 该字段提供了可选的机制,从而允许基于每个容器进行CNI插件的简单配置
- Name of the interface inside the container. 该字段提供了在container (network namespace)中的interface的名字;因此,它也必须符合Linux对于网络命名的限制
- Result:
- Interface list. 根据插件的不同,这个字段可以包括sandbox (container or hypervisor) interface的name,以及host interface的name,每个interface的hardware address,以及interface所在的sandbox(如果存在的话)的信息。
- IP configuration assigned to each interface. IPv4和/或者IPv6地址,gateways以及为sandbox或host interfaces中添加的路由
- DNS inormation. 包含nameservers,domains,search domains和options的DNS information的字典
- Parameters:
- 将container从network中删除(Delete):
- Parameter:
- Version. 调用者使用的CNI 配置的版本信息
- ContainerID. 定义同上
- Network namespace path. 定义同上
- Network configuration. 定义同上
- Extra argument. 定义同上
- Name of the interface inside the container. 定义同上
- Parameter:
- 版本信息
- Parameter: 无
- Result: 返回插件支持的所有CNI版本
{ "cniVersion": "0.3.1", // the version of the CNI spec in use for this output "supportedVersions": [ "0.1.0", "0.2.0", "0.3.0", "0.3.1" ] // the list of CNI spec versions that this plugin supports }
最终executable command-line API会以network的type作为名字去调用相应的插件。它首先会在一系列预先定义好的目录中查找该可执行文件。一旦找到,它就会用以下的环境变量作为参数去调用该可执行文件:
- CNI_COMMAND: 表示进行的操作;ADD, DEL或者VERSION
- CNI_CONTAINERID: Container ID
- CNI_NETNS: network namespace文件的路径
- CNI_IFNAME: 创建的interface的名字,插件必须使用这个名字,否则返回错误
- CNI_ARGS: 在调用时用户传入的额外的参数,由以分号分割的,字母数字键值对组成,例如,"FOO=BAR;ABC=123"
- CNI_PATH:用于查找CNI插件的可执行文件的路径列表,在Linux中,路径之间由":"分割,在Windows中用";"分割
以JSON形式存在的network configuration将以stdin的方式进入插件。这意味着它并不和磁盘上某个特定的文件绑定,因此它所包含的信息也能在每次调用之后发生改变
Result
需要注意的是IPAM插件返回的是一个精简的Result结构,对它的描述放在IP Allocation中
当执行的是ADD命令时,如果返回值是0,并且有如下的JSON输出到stdout,那么说明执行成功了。在IPAM插件返回的结果中同样需要对ips和dns字段进行适当的填充,但是interface字段除外,因为IPAM插件并不应该意识到interface的存在
{ "cniVersion": "0.3.1", "interfaces": [ (this key omitted by IPAM plugins) { "name": "<name>", "mac": "<MAC address>", (required if L2 addresses are meaningful) "sandbox": "<netns path or hypervisor identifier>" (required for container/hypervisor interfaces, empty/omitted for host interfaces) } ], "ips": [ { "version": "<4-or-6>", "address": "<ip-and-prefix-in-CIDR>", "gateway": "<ip-address-of-the-gateway>", (optional) "interface": <numeric index into 'interfaces' list> }, ... ], "routes": [ (optional) { "dst": "<ip-and-prefix-in-cidr>", "gw": "<ip-of-next-hop>" (optional) }, ... ] "dns": { "nameservers": <list-of-nameservers> (optional) "domain": <name-of-local-domain> (optional) "search": <list-of-additional-search-domains> (optional) "options": <list-of-options> (optional) } }
cniVersion以Semantic Version 2.0的格式指定了插件使用的CNI版本。interfaces描述了插件创建的network interfaces。如果指定了CNI_IFNAME,那么插件必须用该名字对sandbox/hypervisor interface进行命名,否则返回错误
- mac (string):interface的hardware address。如果对于插件来说L2地址是没有意义的,那个该字段是可选的
- sandbox (string):container/namespace-based environment需要返回sandbox所在的network namespace的完整路径。Hypervisor/VM-based插件需要返回一个唯一的ID,代表新建的interface所在的virtualized sandbox。
ips字段包含了一系列的IP配置信息,详情参见IP well-known structure。dns字段包含了一个由通用的DNS信息组成的字典。详情参见DNS well-known structure。specification中并没有这些信息到底应该被如何使用。例如产生一个/etc/resolv.conf文件插入容器文件系统中,或者在宿主机运行一个DNS forwarder。
如果遇到错误将得到一个非零的返回值以及如下形式的JSON输出:
{ "cniVersion": "0.3.1", "code": <numeric-error-code>, "msg": <short-error-message>, "details": <long-error-message> (optional) }
cniVersion以Semantic Version 2.0的格式指定了插件使用的CNI版本。Error codes的0-99用于一些众所周知的错误(详情参见Well-known Error Codes)。超过100的值可以用于插件特定的错误。
另外,stderr可以用于输出一些unstructured output,例如logs。
Network Configuration
network configuration以JSON格式进行描述。configuration可以被存储在磁盘中或者通过容器运行时以其他方式产生。接下来是一些比较重要的字段:
- cniVersion(string):cniVersion以Semantic Version 2.0的格式指定了插件使用的CNI版本
- name (string):Network name。这应该在整个管理域中都是唯一的
- type (string):代表了CNI插件可执行文件的文件名
- args (dictionary):由容器运行时提供的可选的参数。比如,可以将一个由label组成的dictionary传递给CNI插件,通过在args下增加一个labels字段
- ipMasqs (boolean):可选项(如果插件支持的话)。为network在宿主机创建IP masquerade。这个字段是必须的,如果需要将宿主机作为网关,从而能够路由到容器分配的IP
- ipam:由特定的IPAM值组成的dictionary
- type (string):表示IPAM插件的可执行文件的文件名
- dns:由特定的DNS值组成的dictionary
- nameservers (list of strings):一系列对network可见的,以优先级顺序排列的DNS nameserver列表。列表中的每一项都包含了一个IPv4或者一个IPv6地址
- domain (string):用于查找short hostname的本地域
- search (list of strings):以优先级顺序排列的用于查找short domain的查找域。对于大多数resolver,它的优先级比domain更高
- options(list of strings):一系列可以被传输给resolver的可选项
插件可能会定义它们自己能接收的额外的字段,但是遇到一个未知的字段可能会产生错误。例外的是args字段,它可以被用于传输一些额外的字段,但可能会被插件忽略
Example configurations
{ "cniVersion": "0.3.1", "name": "dbnet", "type": "bridge", // type (plugin) specific "bridge": "cni0", "ipam": { "type": "host-local", // ipam specific "subnet": "10.1.0.0/16", "gateway": "10.1.0.1" }, "dns": { "nameservers": [ "10.1.0.1" ] } }
{ "cniVersion": "0.3.1", "name": "pci", "type": "ovs", // type (plugin) specific "bridge": "ovs0", "vxlanID": 42, "ipam": { "type": "dhcp", "routes": [ { "dst": "10.3.0.0/16" }, { "dst": "10.4.0.0/16" } ] } // args may be ignored by plugins "args": { "labels" : { "appVersion" : "1.0" } } }
{ "cniVersion": "0.3.1", "name": "wan", "type": "macvlan", // ipam specific "ipam": { "type": "dhcp", "routes": [ { "dst": "10.0.0.0/8", "gw": "10.0.0.1" } ] }, "dns": { "nameservers": [ "10.0.0.1" ] } }
Network Configuration Lists
Network configuration lists能够以指定顺序允许多个CNI插件,并且将每个插件的允许结果传递给下一个插件。列表中包含了一些众所周知的字段以及由一个或多个标准的CNI network configuration组成的列表(如上所示)。
列表以JSON格式描述,可以储存在磁盘中,也可以由容器运行时以其他方式产生。接下来的这些字段是众所周知的并且有对应的含义:
- cniVersion(string):以Semantic Version 2.0描述的CNI版本,对此整个configuration list以及每个单独的configuraion必须遵从
- name (string):Network name。这应该在整个管理域中都是唯一的
- plugins (lists):一系列标准的CNI network configuration dictionary (如上所示)
当执行插件列表时,运行时必须用列表的name和cniVersion字段替代每个network configuraion的name和cniVersion字段。这确保了列表中插件的name和CNI版本都是一致的,从而避免插件之间产生版本冲突。如果插件通过network configuration的capability字段说明它支持某种specific capability,那么运行时必须将capability-based keys以map的形式插入插件的config JSON的runtimeConfig字段中。同时,传给runtimeConfig的key必须和network configuration的capabilities key的名字相同。
对于ADD操作,运行时必须添加一个prevResult字段到下一个插件的configuration JSON中,并且它的内容就是上一个插件的以JSON格式描述的结果。并且每个插件都必须将preResult的内容输出到stdout从而让后续的插件或者运行时可以获取该结果,除非,它们想要修改或限制之前的结果。插件是允许修改或限制全部或者部分的prevResult内容的。然而对于支持包含prevResult的CNI版本的插件,它必须显式地通过,修改或者限制prevResult,但是忽略该字段是不允许的。
同时,运行时必须在同一环境下执行列表中的每个插件
对于DEL操作,运行时必须以相反的顺序执行插件列表
Network Configuration List Error Handling
当在执行插件列表时发生了错误,那么运行时必须停止执行。如果ADD操作执行失败了,当运行时要处理错误时,它需要以和ADD相反的顺序对列表中的插件执行DEL操作,即使其中某些插件在ADD操作中还为被调用。
插件必须完整地执行DEL操作并不报错,即使有些资源缺失了。比如,对于IPAM插件,即使container network namespace 已经不存在了,它仍然会释放IP allocation并且成功返回,除非network namespace对于IPAM特别重要。尽管DHCP会向container network interface发送一个'release' message,但是因为DHCP leases都是有生命周期的,因此release操作并没有那么重要,也就不应该返回错误。另外,对于bridge插件即使container network namespace和/或者container network interface已经不存在了,它也要调用IPAM插件的DEL操作并且删除相应的资源(如果有的话)。
Example network configuration lists
{ "cniVersion": "0.3.1", "name": "dbnet", "plugins": [ { "type": "bridge", // type (plugin) specific "bridge": "cni0", // args may be ignored by plugins "args": { "labels" : { "appVersion" : "1.0" } }, "ipam": { "type": "host-local", // ipam specific "subnet": "10.1.0.0/16", "gateway": "10.1.0.1" }, "dns": { "nameservers": [ "10.1.0.1" ] } }, { "type": "tuning", "sysctl": { "net.core.somaxconn": "500" } } ] }
Network configuration list runtime examples
基于上述的network configuraion list,容器运行时需要执行以下步骤来完成ADD操作。需要注意的是,运行时会将configuration list中的cniVersion和name字段添加到每个插件的configuration中,从而保证列表中所有插件的版本和名字一致。
1、首先以如下格式调用bridge插件
{ "cniVersion": "0.3.1", "name": "dbnet", "type": "bridge", "bridge": "cni0", "args": { "labels" : { "appVersion" : "1.0" } }, "ipam": { "type": "host-local", // ipam specific "subnet": "10.1.0.0/16", "gateway": "10.1.0.1" }, "dns": { "nameservers": [ "10.1.0.1" ] } }
2、接着以如下的JSON调用tuning插件,其中的prevResult字段包含了bridge插件返回的结果
{ "cniVersion": "0.3.1", "name": "dbnet", "type": "tuning", "sysctl": { "net.core.somaxconn": "500" }, "prevResult": { "ips": [ { "version": "4", "address": "10.0.0.5/32", "interface": 0 } ], "dns": { "nameservers": [ "10.1.0.1" ] } } }
给定同样的network configuraion list,容器运行时会以如下步骤完成DEL操作。需要注意的是,并不需要prevResult字段,因为DEL操作并不返回任何result。另外,插件的执行顺序和ADD是相反的。
1、首先以如下JSON调用tuning插件
{ "cniVersion": "0.3.1", "name": "dbnet", "type": "tuning", "sysctl": { "net.core.somaxconn": "500" } }
2、接着以如下JSON调用bridge插件
{ "cniVersion": "0.3.1", "name": "dbnet", "type": "bridge", "bridge": "cni0", "args": { "labels" : { "appVersion" : "1.0" } }, "ipam": { "type": "host-local", // ipam specific "subnet": "10.1.0.0/16", "gateway": "10.1.0.1" }, "dns": { "nameservers": [ "10.1.0.1" ] } }
IP Allocation
作为整个操作的一部分,CNI插件需要给interface分配并维护一个IP地址,并且还要安装一些和该interface有关的必要的路由。这给了CNI插件很大的灵活性同时也给它造成了很大的负担。许多插件需要重复编写多种用户想要的IP管理框架(例如,dhcp, host-local)。为了减轻各个插件的负担,并且将IP管理的功能独立出来,我们定义了第二种插件类型 -- IP Address Management 插件(IPAM插件)。此时,其他插件的任务就是在适当的执行过程中调用相应的IPAM插件。IPAM插件用于确定interface的IP/子网,网关,路由并且将这些信息返回"main" plugin去执行。IPAM插件可以从一个协议(如dhcp)中,或者从本地文件系统存储的数据中,或者network configuration file中的"ipam"字段,或者上述这些方式的组合中获取信息。
IP Address Management (IPAM) Interface
和CNI插件类似,IPAM插件也是以运行可执行文件的方式被调用的。可执行文件将会在一些预先定义的路径列表中查找,通过CNI_PATH指定。IPAM插件将获得所有传输给CNI插件的环境变量,并且和CNI插件一样,IPAM通过stdin获取network configuration
对于ADD命令,如果返回值为0,并且stdout中有如下的JSON格式,说明执行成功
{ "cniVersion": "0.3.1", "ips": [ { "version": "<4-or-6>", "address": "<ip-and-prefix-in-CIDR>", "gateway": "<ip-address-of-the-gateway>" (optional) }, ... ], "routes": [ (optional) { "dst": "<ip-and-prefix-in-cidr>", "gw": "<ip-of-next-hop>" (optional) }, ... ] "dns": { "nameservers": <list-of-nameservers> (optional) "domain": <name-of-local-domain> (optional) "search": <list-of-search-domains> (optional) "options": <list-of-options> (optional) } }
与常规的CNI插件不同的是,IPAM插件返回的是简化的Result结构,其中不包括interfaces字段,因为IPAM插件不应该关注它们的父插件配置的interfaces,那些有特殊要求的IPAM插件除外(例如,dhcp IPAM插件)。
ips字段包含了一系列的IP配置信息,详情参见IP well-known structure
dns字段包含了一个由通用的DNS信息组成的字典。详情参见DNS well-known structure
返回的Errors和logs的和CNI插件相同。详情参见CNI Plugin Result
IPAM插件的例子如下:
- host-local:在一个特定的范围内选择一个未被其他container使用的IP
- dhcp:使用DHCP协议获取并且维护一个IP的租用。DHCP request会通过刚创建的container interface发送出来,因此相关的network必须支持广播
Notes:
- 路由应该被配置为0 metric
- 默认路由应该配置为"0.0.0.0/0"。因为其他的network可能已经配置了默认路由,CNI插件必须能够跳过已有的默认配置
Well-known Structures
IPs:
"ips": [ { "version": "<4-or-6>", "address": "<ip-and-prefix-in-CIDR>", "gateway": "<ip-address-of-the-gateway>", (optional) "interface": <numeric index into 'interfaces' list> (not required for IPAM plugins) }, ... ]
ips字段是一个由插件决定的IP配置列表。每个条目都是都是一个dictionary,描述了一个network interface的IP配置。多个network interface的IP配置和单个interface上的多个IP配置都将以ips列表中的不同条目返回。插件已知的所有特性都要提供,即使并不是严格必须的:
- version (string):"4"或者"6",代表了条目中IP地址的版本。所有的IP地址和网关地址都要符合相应的版本
- address (string):CIDR形式的IP地址(例如,"192.168.1.3/24")
- gateway (string):对应子网的默认网关,如果存在的话。并不要求CNI插件添加任何与网关相关的路由。路由是单独通过routes字段指定的。一个使用该字段的例子是,CNI bridge插件将该地址添加到Linux bridge中,将其作为网关。
- interface (uint):该值表示,此IP配置需要作用的interface,在返回的结果JSON中的interfaces字段中对应的下标。IPAM插件不能返回这个值,因为它们没有任何关于network interface的信息
Routes:
"routes": [ { "dst": "<ip-and-prefix-in-cidr>", "gw": "<ip-of-next-hop>" (optional) }, ... ]
每个routes字段由以下内容组成。所有的IP地址在routes中一定要有相同的IP版本,4或者6
- dst (string):以CIDR描述的目标子网
- gw (string):网关的IP地址。如果该字段不存在,则假设为默认网关(由CNI插件决定)
DNS:
"dns": { "nameservers": <list-of-nameservers> (optional) "domain": <name-of-local-domain> (optional) "search": <list-of-additional-search-domains> (optional) "options": <list-of-options> (optional) }
dns字段包含了由一些通用的DNS信息组成的dictionary
- nameservers (list of strings):一系列对network可见的,以优先级顺序排列的DNS nameserver列表。列表中的每一项都包含了一个IPv4或者一个IPv6地址
- domain (string):用于查找short hostname的本地域
- search (list of strings):以优先级顺序排列的用于查找short domain的查找域。对于大多数resolver,它的优先级比domain更高
- options (list of strings):一系列可以被传输给resolver的可选项
Well-known Error Codes
- 1 - CNI版本不兼容
- 2 - network configuration存在不支持的字段。错误信息中必须包含不支持字段的key和value