0%

【Kubernetes】Container Network Interface

Container Network Interface (CNI) 最早是由 CoreOS 发起的容器网络规范,是Kubernetes网络插件的基础。其基本思想为:Container Runtime在创建容器时,先创建好 network namespace,然后调用 CNI 插件为这个 netns 配置网络,其后再启动容器内的进程。 CNI 作为规范,并不只限于 Kubernetes,在 rkt 、OpenShift 等项目也都被广泛使用。

相对于CNM, Container Network Model CNI 对开发者的约束更少,更开放,不依赖于Docker:

实现一个CNI网络插件只需要 一个配置文件一个可执行的文件

  • 配置文件:描述插件的版本、名称、描述等基本信息
  • 可执行文件:被上层的容器管理平台调用,只需要实现 将容器加入到网络的 ADD 操作将容器从网络中删除的 DEL 操作(以及一个可选的VERSION查看版本操作)

CNI Spec

CNI 接口规范 目标是在 runtimeplugins 之间定义一个规范,plugin 基于这个规范实现自己的网络逻辑,runtime 基于这个规范调用 CNI plugin。

基本抽象

CNI 把容器视作一个 netns,目标就是使得这个 netns 能够真正与外界互通。对于不同类型的 plugin 就通过配置对应的 network 给 container 即可。

container

container 可以看作是一个 linux network namespace

network

Network 代表一组可寻址、互通的设备,配置是 JSON 格式的

接口定义

CNI 的接口中包括以下几个方法:

1
2
3
4
5
6
type CNI interface {
AddNetworkList (net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
DelNetworkList (net *NetworkConfigList, rt *RuntimeConf) error
AddNetwork (net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
DelNetwork (net *NetworkConfig, rt *RuntimeConf) error
}

该接口只有四个方法,添加网络、删除网络、添加网络列表、删除网络列表。类似于操作系统的驱动,将硬件的能力抽象成软件接口:

A driver provides a software interface to hardware devices, enabling operating systems and other computer programs to access hardware functions without needing to know precise details about the hardware being used.

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字段,它可以被用于传输一些额外的字段,但可能会被插件忽略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"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" ]
}
}

可以看到对应的数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type NetworkConfig struct {
Network *types.NetConf
Bytes []byte
}

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:"-"`
}

插件可以扩展自己的参数,以 bridge 插件为例:

github.com/containernetworking/plugins/plugins/main/bridge/bridge.go
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
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"`

Args struct {
Cni BridgeArgs `json:"cni,omitempty"`
} `json:"args,omitempty"`
RuntimeConfig struct {
Mac string `json:"mac,omitempty"`
} `json:"runtimeConfig,omitempty"`

mac string
}

type BridgeArgs struct {
Mac string `json:"mac,omitempty"`
}

配置格式

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操作,运行时必须以相反的顺序执行插件列表

1
2
3
4
5
6
7
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
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"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"
}
}
]
}

CNI Plugin 使用

Using CNI Plugin with Bare Metal

1
2
3
4
5
$ mkdir cni && cd cni
$ curl -O -L https://github.com/containernetworking/cni/releases/download/v0.4.0/cni-amd64-v0.4.0.tgz
$ tar -xzvf cni-amd64-v0.4.0.tgz
$ ls # 可以看到这里解压后实际上是不同插件的二进制文件
bridge cni-amd64-v0.4.0.tgz cnitool dhcp flannel host-local ipvlan loopback macvlan noop ptp tuning

创建一个网络命名空间 cosmos,当前网络命名空间都为空,没有任何网络配置,通过 ip netns exec cosmos ifconfig 查看无响应:

1
2
3
4
$ ip netns add cosmos
$ ip netns list
cosmos
$ ip netns exec cosmos ifconfig # 可以看到此时cosmos网络命名空间为空,无任何配置

接下来创建 mybridge.conf 作为 bridge 插件的配置文件,调用 cni plugincosmos 这个 network namespace ADD 到 network 上,这也就对应着将容器 Add 到某个网络上。可以看到在 mybridge.conf 中定义了这个命名空间的 subnet 和 路由表,同时加入网络时通过环境变量指定了网口名称等信息:

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
$ cat > mybridge.conf <<"EOF"
{
"cniVersion": "0.2.0",
"name": "mybridge",
"type": "bridge",
"bridge": "cni_bridge0",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"subnet": "10.15.20.0/24",
"routes": [
{ "dst": "0.0.0.0/0" },
{ "dst": "1.1.1.1/32", "gw":"10.15.20.1"}
]
}
}
EOF

$ CNI_COMMAND=ADD CNI_CONTAINERID=cosmos CNI_NETNS=/var/run/netns/cosmos CNI_IFNAME=eth12 CNI_PATH=`pwd` ./bridge < mybridge.conf
2021/01/28 10:55:02 Error retriving last reserved ip: Failed to retrieve last reserved ip: open /var/lib/cni/networks/mybridge/last_reserved_ip: no such file or directory
{
"ip4": {
"ip": "10.15.20.2/24",
"gateway": "10.15.20.1",
"routes": [
{
"dst": "0.0.0.0/0"
},
{
"dst": "1.1.1.1/32",
"gw": "10.15.20.1"
}
]
},
"dns": {}
}

这里返回了一个错误是因为 IPAM 驱动不能够找到它本地存储 IP 信息的文件,这个文件将在第一次运行上述命令时创建。如果你在另一个网络命名空间再运行一次上述命令,就不会再得到上述错误了。命令执行结果返回了一个 JSON 文件,其中表明了插件相关的网络配置。在这里,bridge 会收到 10.15.20.1/24 作为 IP 地址,而 网络命令空间的 Interface 收到 10.15.20.1/24 。同时,bridge 插件也配置了 1.1.1.1/32 的路由。

所有CNI插件均支持通过环境变量和标准输入传入参数:

1
2
3
$ echo '{"cniVersion": "0.3.1","name": "mynet","type": "macvlan","bridge": "cni0","isGateway": true,"ipMasq": true,"ipam": {"type": "host-local","subnet": "10.244.1.0/24","routes": [{ "dst": "0.0.0.0/0" }]}}' | sudo CNI_COMMAND=ADD CNI_NETNS=/var/run/netns/a CNI_PATH=./bin CNI_IFNAME=eth0 CNI_CONTAINERID=a CNI_VERSION=0.3.1 ./bin/bridge

$ echo '{"cniVersion": "0.3.1","type":"IGNORED", "name": "a","ipam": {"type": "host-local", "subnet":"10.1.2.3/24"}}' | sudo CNI_COMMAND=ADD CNI_NETNS=/var/run/netns/a CNI_PATH=./bin CNI_IFNAME=a CNI_CONTAINERID=a CNI_VERSION=0.3.1 ./bin/host-local

接下来再来查看 cosmos 这个网络命名空间的配置,可以看到相比于原来,现在多了两个网络接口,一个是先创建的 cni_bridge0 网桥,另一个是 veth 对的一边。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ ifconfig
cni_bridge0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 10.15.20.1 netmask 255.255.255.0 broadcast 0.0.0.0
inet6 fe80::3064:b8ff:fe98:6b0c prefixlen 64 scopeid 0x20<link>
ether 0a:58:0a:0f:14:01 txqueuelen 1000 (Ethernet)
RX packets 9 bytes 600 (600.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 30 bytes 4430 (4.3 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

veth0311918a: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet6 fe80::8029:32ff:fe55:6cbf prefixlen 64 scopeid 0x20<link>
ether 82:29:32:55:6c:bf txqueuelen 0 (Ethernet)
RX packets 9 bytes 726 (726.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 38 bytes 5066 (4.9 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
...

接下来我们到 Network Namespace Interface 里面去看看,可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ip netns exec cosmos ifconfig
eth12: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 10.15.20.3 netmask 255.255.255.0 broadcast 0.0.0.0
inet6 fe80::2460:91ff:fe20:8a0 prefixlen 64 scopeid 0x20<link>
ether 0a:58:0a:0f:14:03 txqueuelen 0 (Ethernet)
RX packets 11 bytes 906 (906.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 10 bytes 796 (796.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

$ ip netns exec cosmos ip route
default via 10.15.20.1 dev eth12
1.1.1.1 via 10.15.20.1 dev eth12
10.15.20.0/24 dev eth12 proto kernel scope link src 10.15.20.3

这个例子并没有什么实际的价值,但将 cni plugin 操作 network namespace 从 cni 繁杂的上下文中抽取出来,让我们看到它最本来的样子。

Using CNI with Ccontainer Runtime

net=none 创建的容器,为其配置网络 与上文的为 network namespace 配置网络是一样的。

首先我们通过 docker run 运行一个容器,这里指定 net=none 告诉 Docker 为容器去创建一个网络命名空间,但是并不将这个网络命名空间挂到其他的任何地方。通过 ifconfig 查看容器内部网络,只能看到一个本地回环网口。这个时候我们是访问不了容器的内部服务的,因为我们根本不知道其地址。

1
2
3
4
5
6
7
8
9
$ docker run --name cnitest --net=none -d jonlangemak/web_server_1
$ docker exec cnitest ifconfig
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)

接下来我们像上面一样,使用 CNI 将为容器设置网络,在此之前我们需要知道容器的网络命名空间和容器ID:

1
2
3
$ docker inspect cnitest | grep -E 'SandboxKey|Id'
"Id": "988f72b8069ed381243ca8e06d25e9bf9c1482d11fa7baf4ed76a8b0cac0c3e2",
"SandboxKey": "/var/run/docker/netns/0582ad9f14ef",

同样,设置 CNI 配置文件,然后启动 bridge 这个插件,通过环境变量为其传递参数:

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
$ cat > mybridge2.conf <<"EOF"
{
"cniVersion": "0.2.0",
"name": "mybridge",
"type": "bridge",
"bridge": "cni_bridge1",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"subnet": "10.15.30.0/24",
"routes": [
{ "dst": "0.0.0.0/0" },
{ "dst": "1.1.1.1/32", "gw":"10.15.30.1"}
],
"rangeStart": "10.15.30.100",
"rangeEnd": "10.15.30.200",
"gateway": "10.15.30.99"
}
}
EOF

$ CNI_COMMAND=ADD CNI_CONTAINERID=988f72b8069ed381243ca8e06d25e9bf9c1482d11fa7baf4ed76a8b0cac0c3e2 CNI_NETNS=/var/run/docker/netns/0582ad9f14ef CNI_IFNAME=eth0 CNI_PATH=`pwd` ./bridge < mybridge2.conf
{
"ip4": {
"ip": "10.15.30.100/24",
"gateway": "10.15.30.99",
"routes": [
{
"dst": "0.0.0.0/0"
},
{
"dst": "1.1.1.1/32",
"gw": "10.15.30.1"
}
]
},
"dns": {}
}

这时候再在主机上查看,我们可以看到主机上多了一个 cni_bridge1 和另一个 veth 端。 cni_bridge1 的 IP 地址就是我们在上面设置的 gateway 地址。

1
2
3
4
5
6
7
8
cni_bridge1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
inet 10.15.30.99 netmask 255.255.255.0 broadcast 0.0.0.0
inet6 fe80::ec4e:c5ff:feed:729f prefixlen 64 scopeid 0x20<link>
ether 0a:58:0a:0f:1e:63 txqueuelen 1000 (Ethernet)
RX packets 6 bytes 681 (681.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 41 bytes 5322 (5.1 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

至此容器网络配置完毕,我们可以进入容器内部查看网络设置,可以看到:

  • 容器内部 IP 地址就是 IPAM 定义的起始地址 10.15.30.100
  • 容器内部网络设备名称为设置的 eth0
  • 容器内部网络路由默认由网关 10.15.30.99 处理
  • 将对 1.1.1.1/32 的访问路由到设置的网关 10.15.30.1 处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ docker exec cnitest ifconfig
eth0 Link encap:Ethernet HWaddr 0a:58:0a:0f:1e:64
inet addr:10.15.30.100 Bcast:0.0.0.0 Mask:255.255.255.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:36 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:4912 (4.9 KB) TX bytes:0 (0.0 B)

lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
$ docker exec cnitest ip route
default via 10.15.30.99 dev eth0
1.1.1.1 via 10.15.30.1 dev eth0
10.15.30.0/24 dev eth0 proto kernel scope link src 10.15.30.100

到现在,容器已经有其 IP 地址,直接访问这个地址可以得到容器内部服务的响应:

1
2
3
4
5
6
$ curl http://10.15.30.100
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">Web Server #1 - Running on port 80</span></h1>
</body>
</html>

Using CNI with CRI

CNI插件作为可执行文件,会被kubelet调用。启动kubelet --network-plugin=cni,--cni-conf-dir 指定 networkconfig 配置,默认路径是:/etc/cni/net.d,并且,--cni-bin-dir 指定plugin可执行文件路径,默认路径是:/opt/cni/bin

Kubernetes Pod 中的其他容器都是Pod所属 pause 容器的网络,创建过程为:

  1. CRI runtime 先创建 pause 容器生成 network namespace
  2. 调用网络 CNI driver
  3. CNI driver 根据配置调用具体的 CNI 插件
  4. CNI 插件给 pause 容器配置网络
  5. Pod 中其他的容器都使用 pause 容器的网络

在 Kubernetes 中,处理容器网络相关的逻辑并不会在 kubelet 主干代码里执行,而是会在具体的 CRI 实现里完成。

CRI 设计的一个重要原则,就是确保这个接口本身只关注容器,不关注Pod。但 CRI 里有一个 PodSandbox,抽取了 Pod 里的一部分与容器运行时相关的字段,比如 Hostname、DnsConfig 等。作为具体的容器项目,自己决定如何使用这些字段来实现一个 k8s 期望的 Pod 模型。

当 kubelet 组件需要创建 Pod 的时候,它第一个创建的一定是 Infra 容器,这体现在上图的 RunPodSandbox 中

RunPodSandbox

RunPodSandbox creates and starts a pod-level sandbox. Runtimes should ensure the sandbox is in ready state.For docker, PodSandbox is implemented by a container holding the network namespace for the pod.Note: docker doesn’t use LogDirectory (yet).

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
func (ds *dockerService) RunPodSandbox(ctx context.Context, r *runtimeapi.RunPodSandboxRequest) (*runtimeapi.RunPodSandboxResponse, error) {
config := r.GetConfig()
// Step 1: Pull the image for the sandbox.
err := ensureSandboxImageExists(ds.client, defaultSandboxImage);
// Step 2: Create the sandbox container.
createConfig, err := ds.makeSandboxDockerConfig(config, image)
createResp, err := ds.client.CreateContainer(*createConfig)
ds.setNetworkReady(createResp.ID, false)
defer func(e *error) {
// Set networking ready depending on the error return of the parent function
if *e == nil {
ds.setNetworkReady(createResp.ID, true)
}
}(&err)
// Step 3: Create Sandbox Checkpoint.
ds.checkpointManager.CreateCheckpoint(createResp.ID, constructPodSandboxCheckpoint(config));
// Step 4: Start the sandbox container. Assume kubelet's garbage collector would remove the sandbox later, if startContainer failed.
err = ds.client.StartContainer(createResp.ID)
// Rewrite resolv.conf file generated by docker.
containerInfo, err := ds.client.InspectContainer(createResp.ID)
err := rewriteResolvFile(containerInfo.ResolvConfPath, dnsConfig.Servers, dnsConfig.Searches, dnsConfig.Options);
// Do not invoke network plugins if in hostNetwork mode.
if config.GetLinux().GetSecurityContext().GetNamespaceOptions().GetNetwork() == runtimeapi.NamespaceMode_NODE {
return resp, nil
}
// Step 5: Setup networking for the sandbox.
// All pod networking is setup by a CNI plugin discovered at startup time. This plugin assigns the pod ip, sets up routes inside the sandbox,
// creates interfaces etc. In theory, its jurisdiction ends with pod sandbox networking, but it might insert iptables rules or open ports
// on the host as well, to satisfy parts of the pod spec that aren't recognized by the CNI standard yet.
err = ds.network.SetUpPod(config.GetMetadata().Namespace, config.GetMetadata().Name, cID, config.Annotations, networkOptions)
return resp, nil
}

Init CNI plugin

cniNetworkPlugin.Init 方法逻辑如下

1
2
3
4
5
6
7
8
9
10
11
12
13
func (plugin *cniNetworkPlugin) Init(host network.Host, hairpinMode kubeletconfig.HairpinMode, nonMasqueradeCIDR string, mtu int) error {
err := plugin.platformInit()
...
plugin.host = host
plugin.syncNetworkConfig()
return nil
}

func (plugin *cniNetworkPlugin) syncNetworkConfig() {
network, err := getDefaultCNINetwork(plugin.confDir, plugin.binDirs)
...
plugin.setDefaultNetwork(network)
}

confDir 加载 xx.conflist,结合 binDirs 构造 defaultNetwork

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func getDefaultCNINetwork(confDir string, binDirs []string) (*cniNetwork, error) {
files, err := libcni.ConfFiles(confDir, []string{".conf", ".conflist", ".json"})
sort.Strings(files)
for _, confFile := range files {
var confList *libcni.NetworkConfigList
if strings.HasSuffix(confFile, ".conflist") {
confList, err = libcni.ConfListFromFile(confFile)
...
}
network := &cniNetwork{
name: confList.Name,
NetworkConfig: confList,
CNIConfig: &libcni.CNIConfig{Path: binDirs},
}
return network, nil
}
return nil, fmt.Errorf("No valid networks found in %s", confDir)
}

SetUpPod

可以看到 CNI Plugin 分别设置 local 网口 和默认的网口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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)
}

// Todo get the timeout from parent ctx
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
}
}

_, err = plugin.addToNetwork(cniTimeoutCtx, plugin.getDefaultNetwork(), name, namespace, id, netnsPath, annotations, options)
return err
}

addToNetwork

addToNetwork 这里获取了 RuntimeConfNetworkConfig,这个 cniNet 即使 CNI 类型的接口:

kubernetes/pkg/kubelet/dockershim/network/cni/cni.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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) {
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
klog.V(4).Infof("Adding %s to network %s/%s netns %q", pdesc, netConf.Plugins[0].Network.Type, netConf.Name, podNetnsPath)
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
}

AddNetworkList

这里对于每个 plugin 真正调用了 addNetwork

github.com/containernetworking/cni/libcni/api.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// AddNetworkList executes a sequence of plugins with the ADD command
func (c *CNIConfig) AddNetworkList(ctx context.Context, list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) {
var err error
var result types.Result
for _, net := range list.Plugins {
result, err = c.addNetwork(ctx, list.Name, list.CNIVersion, net, result, rt)
if err != nil {
return nil, err
}
}

if err = c.cacheAdd(result, list.Bytes, list.Name, rt); err != nil {
return nil, fmt.Errorf("failed to set network %q cached result: %v", list.Name, err)
}

return result, nil
}

ExecPlugin

CNIConfig 接收到指令后, 拼凑 shell 指令及参数执行 cni binary 文件。CNI 插件的初始化就是 根据 binary path 初始化CNIConfig,进而初始化 NetworkPlugin。至于 cni binary 本身只需要执行时运行即可,就像 go 运行一般的可执行文件一样

github.com/containernetworking/cni/pkg/invoke/raw_exec.go
1
2
3
4
5
6
7
8
9
10
11
12
func (e *RawExec) ExecPlugin(ctx context.Context, pluginPath string, stdinData []byte, environ []string) ([]byte, error) {
stdout := &bytes.Buffer{}
c := exec.CommandContext(ctx, pluginPath)
c.Env = environ
c.Stdin = bytes.NewBuffer(stdinData)
c.Stdout = stdout
c.Stderr = e.Stderr
if err := c.Run(); err != nil {
return nil, pluginErr(err, stdout.Bytes())
}
return stdout.Bytes(), nil
}

CNI Plugin 实现

skel 初始化

为了方便开发者实现自己的 CNI 插件,CNI 在 github.com/containernetworking/cni/pkg/skel/skel.go 定义了一些通用的数据结构和函数。执行时执行plugin main 方法 ==> skel.PluginMain ==> dispatcher.pluginMain ,CmdArgs 约定了调用参数。任何插件都是这样一个套路,只需实现cmdAdd 和 cmdDel 即可,用于对应添加网卡和删除网卡两个操作。

1
2
3
4
5
6
func main() {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, cniSpecVersion.PluginSupports("0.1.0", "0.2.0", "0.3.0", "0.3.1"), "Demo CNI plugin")
}

func cmdAdd(args *skel.CmdArgs) error {...}
func cmdDel(args *skel.CmdArgs) error {...}

插件执行的结果以 STDOUT/STDERR 的形式输出出来,根据 COMMAD 的类型,调用对应的函数:

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
func (t *dispatcher) pluginMain(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) *types.Error {
cmd, cmdArgs, err := t.getCmdArgsFromEnv()

/* ... */

switch cmd {
case "ADD":
err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdAdd)
case "CHECK":
err := t.checkVersionAndCall(cmdArgs, versionInfo, cmdCheck)
/* ... */
case "DEL":
err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdDel)
case "VERSION":
if err := versionInfo.Encode(t.Stdout); err != nil {
return types.NewError(types.ErrIOFailure, err.Error(), "")
}
default:
return types.NewError(types.ErrInvalidEnvironmentVariables, fmt.Sprintf("unknown CNI_COMMAND: %v", cmd), "")
}

if err != nil {
return err
}
return nil
}

如前所述,CNI 插件作为二进制,会从环境变量中接收参数,然后构建出自己的 CmdArgs

github.com/containernetworking/cni/pkg/skel/skel.go
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
func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, *types.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",
&contID,
reqForCmdEntry{
"ADD": true,
"CHECK": true,
"DEL": true,
},
},
{
"CNI_NETNS",
&netns,
reqForCmdEntry{
"ADD": true,
"CHECK": true,
"DEL": false,
},
},
{
"CNI_IFNAME",
&ifName,
reqForCmdEntry{
"ADD": true,
"CHECK": true,
"DEL": true,
},
},
{
"CNI_ARGS",
&args,
reqForCmdEntry{
"ADD": false,
"CHECK": false,
"DEL": false,
},
},
{
"CNI_PATH",
&path,
reqForCmdEntry{
"ADD": true,
"CHECK": true,
"DEL": true,
},
},
}

/* ... */

stdinData, err := ioutil.ReadAll(t.Stdin)
if err != nil {
return "", nil, types.NewError(types.ErrIOFailure, fmt.Sprintf("error reading from stdin: %v", err), "")
}

cmdArgs := &CmdArgs{
ContainerID: contID,
Netns: netns,
IfName: ifName,
Args: args,
Path: path,
StdinData: stdinData,
}
return cmd, cmdArgs, nil
}

这里的参数主要包括容器的 ContainerIDNetnsIfName 等,还有一个 StdinData 即是传入的配置文件:

github.com/containernetworking/cni/pkg/skel/skel.go
1
2
3
4
5
6
7
8
9
10
// CmdArgs captures all the arguments passed in to the plugin
// via both env vars and stdin
type CmdArgs struct {
ContainerID string
Netns string
IfName string
Args string
Path string
StdinData []byte
}

cmdAdd

这里以 bridge plugin 为例讲解 CNI 的实现。

loadNetConf

loadNetConf 主要是从标准输入得到NetConf结构体配置信息

1
2
3
4
5
6
7
8
9
10
11
12
func loadNetConf(bytes []byte, envArgs string) (*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)
}

/* ... */

return n, n.CNIVersion, nil
}

setupBridge

  • setupBridge 里面调用 ensureBridge,前面吧啦吧啦设置了一大队系统调用参数,
  • 通过 netlink.LinkAdd(br) 创建网桥,相当于 ip link add br-test type bridge
  • 然后通过 netlink.LinkSetUp(br) 启动网桥,相当于 ip link set dev br-test up
1
2
3
4
5
6
7
8
9
10
11
12
13
func setupBridge(n *NetConf) (*netlink.Bridge, *current.Interface, error) {
/* ... */
// 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, &current.Interface{
Name: br.Attrs().Name,
Mac: br.Attrs().HardwareAddr.String(),
}, nil
}

实际创建网桥和启动网桥的代码,通过 netlink 实现:

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
func ensureBridge(brName string, mtu int, promiscMode, vlanFiltering bool) (*netlink.Bridge, error) {
br := &netlink.Bridge{
LinkAttrs: netlink.LinkAttrs{
Name: brName,
MTU: mtu,
TxQLen: -1,
},
}

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)
}
}

// Re-fetch link to read all attributes and if it already existed,
// ensure it's really a bridge with similar configuration
br, err = bridgeByName(brName)
if err != nil {
return nil, err
}

if err := netlink.LinkSetUp(br); err != nil {
return nil, err
}

return br, nil
}

setupVeth

  • 调用 netlink.LinkAdd(veth) 创建 veth,这个是一个管道,Linux的网卡对,在容器对应的namespace下创建好虚拟网络接口,相当于 ip link add test-veth0 type veth peer name test-veth1
  • 调用 netlink.LinkSetUp(contVeth) 启动容器端网卡,相当于 ip link set dev test-veth0 up
  • 调用 netlink.LinkSetNsFd(hostVeth, int(hostNS.Fd())) 将 host 端加入 namespace 中,相当于ip link set $link netns $ns
  • 调用 netlink.LinkSetMaster(hostVeth, br) 绑到 bridge,相当于 ip link set dev test-veth0 master br-test
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
func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool, vlanID int, mac string) (*current.Interface, *current.Interface, error) {
contIface := &current.Interface{}
hostIface := &current.Interface{}

err := netns.Do(func(hostNS ns.NetNS) error {
// create the veth pair in the container and move host end into host netns
hostVeth, containerVeth, err := ip.SetupVeth(ifName, mtu, mac, 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
}

// need to lookup hostVeth again as its index has changed during ns move
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()

// connect host veth end to the bridge
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)
}

return hostIface, contIface, nil
}

创建 veth pair 的代码:

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
func SetupVethWithName(contVethName, hostVethName string, mtu int, contVethMac string, hostNS ns.NetNS) (net.Interface, net.Interface, error) {
hostVethName, contVeth, err := makeVeth(contVethName, hostVethName, mtu, contVethMac, hostNS)
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)
}

var hostVeth netlink.Link
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
}

ipam.ExecAdd

1
2
3
4
5
6
7
8
9
10
11
  // run the IPAM plugin and get back the config to apply
r, err := ipam.ExecAdd(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
}

result.IPs = ipamResult.IPs
result.Routes = ipamResult.Routes
1
2
3
func ExecAdd(plugin string, netconf []byte) (types.Result, error) {
return invoke.DelegateAdd(context.TODO(), plugin, netconf, nil)
}

calcGateways

调用calcGateways根据IP地址计算对应的路由和网关

1
2
 // Gather gateway information for each IP family
gwsV4, gwsV6, err := calcGateways(result, n)

ipam.ConfigureIface

  • 调用 ipam.ConfigureIface 将IP地址设置到对应的虚拟网络接口上,相当于 ifconfig test-veth0 192.168.209.135/24 up
  • 调用 enableIPForward(gws.family) 开启ip转发,路径 /proc/sys/net/ipv4/ip_forward 写入值1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Configure the container hardware address and IP address(es)
if err := netns.Do(func(_ ns.NetNS) error {
// Disable IPv6 DAD just in case hairpin mode is enabled on the
// bridge. Hairpin mode causes echos of neighbor solicitation
// packets, which causes DAD failures.
for _, ipc := range result.IPs {
if ipc.Address.IP.To4() == nil && (n.HairpinMode || n.PromiscMode) {
if err := disableIPV6DAD(args.IfName); err != nil {
return err
}
break
}
}

// Add the IP to the interface
if err := ipam.ConfigureIface(args.IfName, result); err != nil {
return err
}
return nil
}); err != nil {
return err
}

ip.SetupIPMasq

调用 ip.SetupIPMasq 建立 iptables 规则

1
2
3
4
5
6
7
8
9
if n.IPMasq {
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
}
}
}

cmdDel

cmdDel 则是对于 interface 的销毁。

cmdCheck

cmdCheck 主要是对于 interface 的检查。

插件分类

Main:接口创建

bridge

Bridge是最简单的CNI网络插件,它首先在Host创建一个网桥,然后再通过 veth pair 连接该网桥到 container netns。

img

注意:Bridge模式下,多主机网络通信需要额外配置主机路由,或使用overlay网络。可以借助Flannel或者Quagga动态路由等来自动配置。比如overlay情况下的网络结构为

img

配置示例

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
{
"cniVersion": "0.3.0",
"name": "mynet",
"type": "bridge",
"bridge": "mynet0",
"isDefaultGateway": true,
"forceAddress": false,
"ipMasq": true,
"hairpinMode": true,
"ipam": {
"type": "host-local",
"subnet": "10.10.0.0/16"
}
}
# export CNI_PATH=/opt/cni/bin
# ip netns add ns
# /opt/cni/bin/cnitool add mynet /var/run/netns/ns
{
"interfaces": [
{
"name": "mynet0",
"mac": "0a:58:0a:0a:00:01"
},
{
"name": "vethc763e31a",
"mac": "66:ad:63:b4:c6:de"
},
{
"name": "eth0",
"mac": "0a:58:0a:0a:00:04",
"sandbox": "/var/run/netns/ns"
}
],
"ips": [
{
"version": "4",
"interface": 2,
"address": "10.10.0.4/16",
"gateway": "10.10.0.1"
}
],
"routes": [
{
"dst": "0.0.0.0/0",
"gw": "10.10.0.1"
}
],
"dns": {}
}
# ip netns exec ns ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
9: eth0@if8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 0a:58:0a:0a:00:04 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.10.0.4/16 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::8c78:6dff:fe19:f6bf/64 scope link tentative dadfailed
valid_lft forever preferred_lft forever
# ip netns exec ns ip route
default via 10.10.0.1 dev eth0
10.10.0.0/16 dev eth0 proto kernel scope link src 10.10.0.4

macvlan

创建一个新的 MAC 地址,将所有的流量转发到容器

ipvlan

在容器中添加一个 ipvlan 接口

host-device

loopback

创建一个回环接口

ptp

创建 veth 对

ptp插件通过veth pair给容器和host创建点对点连接:veth pair一端在container netns内,另一端在host上。可以通过配置host端的IP和路由来让ptp连接的容器之前通信。

1
2
3
4
5
6
7
8
9
10
11
{
"name": "mynet",
"type": "ptp",
"ipam": {
"type": "host-local",
"subnet": "10.1.1.0/24"
},
"dns": {
"nameservers": [ "10.1.1.1", "8.8.8.8" ]
}
}

vlan

分配一个 vlan 设备

IPAM:IP 地址分配

作为容器网络管理的一部分,CNI 插件需要为接口分配(并维护)IP 地址,并安装与该接口相关的所有必要路由。这给了 CNI 插件很大的灵活性,但也给它带来了很大的负担。众多的 CNI 插件需要编写相同的代码来支持用户需要的多种 IP 管理方案(例如 dhcp、host-local)。

为了减轻负担,使 IP 管理策略与 CNI 插件类型解耦,我们定义了 IP 地址管理插件(IPAM 插件)。CNI 插件的职责是在执行时恰当地调用 IPAM 插件。 IPAM 插件必须确定接口 IP/subnet,网关和路由,并将此信息返回到 “主” 插件来应用配置。 IPAM 插件可以通过协议(例如 dhcp)、存储在本地文件系统上的数据、网络配置文件的 “ipam” 部分或上述的组合来获得信息。

像 CNI 插件一样,调用 IPAM 插件的可执行文件。可执行文件位于预定义的路径列表中,通过 CNI_PATH 指示给 CNI 插件。 IPAM 插件必须接收所有传入 CNI 插件的相同环境变量。就像 CNI 插件一样,IPAM 插件通过 stdin 接收网络配置。

DHCP

在主机上运行守护程序,代表容器发出 DHCP 请求

DHCP插件是最主要的IPAM插件之一,用来通过DHCP方式给容器分配IP地址,在macvlan插件中也会用到DHCP插件。

在使用DHCP插件之前,需要先启动dhcp daemon:

1
/opt/cni/bin/dhcp daemon &

然后配置网络使用dhcp作为IPAM插件

1
2
3
4
5
6
{
...
"ipam": {
"type": "dhcp",
}
}

host-local

host-local是最常用的 CNI IPAM 插件,用来给 container 分配IP地址。

IPv4 配置实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"ipam": {
"type": "host-local",
"subnet": "10.10.0.0/16",
"rangeStart": "10.10.1.20",
"rangeEnd": "10.10.3.50",
"gateway": "10.10.0.254",
"routes": [
{ "dst": "0.0.0.0/0" },
{ "dst": "192.168.0.0/16", "gw": "10.10.5.1" }
],
"dataDir": "/var/my-orchestrator/container-ipam-state"
}
}

static

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
{
"ipam": {
"type": "static",
"addresses": [
{
"address": "10.10.0.1/24",
"gateway": "10.10.0.254"
},
{
"address": "3ffe:ffff:0:01ff::1/64",
"gateway": "3ffe:ffff:0::1"
}
],
"routes": [
{ "dst": "0.0.0.0/0" },
{ "dst": "192.168.0.0/16", "gw": "10.10.5.1" },
{ "dst": "3ffe:ffff:0:01ff::1/64" }
],
"dns": {
"nameservers" : ["8.8.8.8"],
"domain": "example.com",
"search": [ "example.com" ]
}
}
}

Meta:其它插件

tuning

调整现有接口的 sysctl 参数

portmap

一个基于 iptables 的 portmapping 插件。将端口从主机的地址空间映射到容器。

bandwidth

sbr

firewall

CNI Plugin Chains

CNI 还支持 Plugin Chains,即指定一个插件列表,由 Runtime 依次执行每个插件。这对支持端口映射(portmapping)、虚拟机等非常有帮助。

Network Configuration Lists

CNI SPEC 支持指定网络配置列表,包含多个网络插件,由 Runtime 依次执行。注意

  • ADD 操作,按顺序依次调用每个插件;而 DEL 操作调用顺序相反
  • ADD 操作,除最后一个插件,前面每个插件需要增加 prevResult 传递给其后的插件
  • 第一个插件必须要包含 ipam 插件

端口映射示例

下面的例子展示了 bridge+portmap 插件的用法。

首先,配置 CNI 网络使用 bridge+portmap 插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# cat /root/mynet.conflist
{
"name": "mynet",
"cniVersion": "0.3.0",
"plugins": [
{
"type": "bridge",
"bridge": "mynet",
"ipMasq": true,
"isGateway": true,
"ipam": {
"type": "host-local",
"subnet": "10.244.10.0/24",
"routes": [
{"dst": "0.0.0.0/0"}
]
}
},
{
"type": "portmap",
"capabilities": {"portMappings": true}
}
]
}

然后通过 CAP_ARGS 设置端口映射参数:

1
2
3
4
5
6
7
8
9
10
# export CAP_ARGS='{
"portMappings": [
{
"hostPort": 9090,
"containerPort": 80,
"protocol": "tcp",
"hostIP": "127.0.0.1"
}
]
}'

测试添加网络接口:

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
# ip netns add test
# CNI_PATH=/opt/cni/bin NETCONFPATH=/root ./cnitool add mynet /var/run/netns/test
{
"interfaces": [
{
"name": "mynet",
"mac": "0a:58:0a:f4:0a:01"
},
{
"name": "veth2cfb1d64",
"mac": "4a:dc:1f:b7:56:b1"
},
{
"name": "eth0",
"mac": "0a:58:0a:f4:0a:07",
"sandbox": "/var/run/netns/test"
}
],
"ips": [
{
"version": "4",
"interface": 2,
"address": "10.244.10.7/24",
"gateway": "10.244.10.1"
}
],
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"dns": {}
}

可以从 iptables 规则中看到添加的规则:

1
2
3
# iptables-save | grep 10.244.10.7
-A CNI-DN-be1eedf7a76853f303ebd -d 127.0.0.1/32 -p tcp -m tcp --dport 9090 -j DNAT --to-destination 10.244.10.7:80
-A CNI-SN-be1eedf7a76853f303ebd -s 127.0.0.1/32 -d 10.244.10.7/32 -p tcp -m tcp --dport 80 -j MASQUERADE

最后,清理网络接口:

1
# CNI_PATH=/opt/cni/bin NETCONFPATH=/root ./cnitool del mynet /var/run/netns/test

参考资料