Understanding CNI (Container Networking Interface)
If you’ve been paying attention to the discussions around container networking you’ve likely heard the acronym CNI being used. CNI stands for Container Networking Interface and it&#821…

0x00 前言

说到容器就不能不提 CNI,CNI 作为容器网络的统一标准,让各个容器管理平台(k8s,mesos等)都可以通过相同的接口调用各式各样的网络插件(flannel,calico,weave 等)来为容器配置网络。容器管理系统与网络插件之间的关系图如下所示。
1
我们可以发现其实 CNI 定义的就是一组容器运行时(containerd,rkt 等)与网络插件之间的规范定义,所以我们如果要想深入到容器网络插件的开发,了解 CNI 是必须的。

正好最近看了一篇入门 CNI(Container Network Interface) 的博客(原文见文章顶部),觉得写的还挺不错的,但是没有找到中文文档,所以尝试着翻译下分享给需要的人,水平有限,不足之处多多指教。

0x01 正文

如果您一直有关注容器网络的讨论的话,您可能听说过CNI。CNI全称叫Container Network Interface(容器网络接口),其目标是为容器创建基于插件的通用网络解决方案。CNI的说明在规范(立即去阅读,不会耗费你太多时间的)中定义了,以下是我在初读CNI规范时了解到的一些要点...

  • CNI 规范将为一个容器定义一个Linux网络命名空间。我们应该熟悉这种定义,因为像Docker这样的容器运行时会为每个Docker容器都创建一个新的网络命名空间。
  • CNI的网络定义存储为JSON格式。
  • 网络定义通过STDIN输入流传输到插件,这意味着宿主机上不会存储网络配置文件。
  • 其他的配置参数通过环境变量传递给插件
  • CNI插件为可执行文件。
  • CNI插件负责连通容器网络,也就是说,它要完成所有的工作才能使容器连入网络。在Docker中,这些工作包括以某种方式将容器网络命名空间连接回宿主机。
  • CNI插件负责调用IPAM插件,IPAM负责IP地址分配和设置容器所需的路由。

如果您平时习惯和 Docker 打交道,CNI 看上去似乎并不怎么适用。CNI 插件是负责容器网络的,这很显而易见,但一开始它是如何实现的我并不清楚。所以我的下一个问题是,我可以将CNI与Docker一起使用吗?答案是肯定的,但这不是个完整的解决方案。Docker有自己的 CNM 标准,CNM允许插件直接与Docker交互,并可以将CNM插件注册到Docker并直接使用。也就是说,您可以使用Docker运行容器,并将其网络直接分配给CNM注册的插件。这很好,但是因为Docker具有CNM,所以它们不直接与CNI集成(据我所知)。但是,这并不意味着您不能将CNI与Docker一起使用,再回到上面第六点看看,它说了CNI插件负责连接容器,因此有可能只是用Docker的容器的运行时,而不调用 Docker 的网络端的工作(在以后的文章中将对此进行更多介绍)。

在这一点上,我认为有必要了解下 CNI 具体干了些什么,以便更好地了解其是如何与容器结合的。让我们看一个使用插件的简单示例。

首先我们下载预构建的 CNI 二进制文件...

user@ubuntu-1:~$ mkdir cni
user@ubuntu-1:~$ cd cni
user@ubuntu-1:~/cni$ curl -O -L https://github.com/containernetworking/cni/releases/download/v0.4.0/cni-amd64-v0.4.0.tgz
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   597    0   597    0     0   1379      0 --:--:-- --:--:-- --:--:--  1381
100 15.3M  100 15.3M    0     0  4606k      0  0:00:03  0:00:03 --:--:-- 5597k
user@ubuntu-1:~/cni$
user@ubuntu-1:~/cni$ tar -xzvf cni-amd64-v0.4.0.tgz
./
./macvlan
./dhcp
./loopback
./ptp
./ipvlan
./bridge
./tuning
./noop
./host-local
./cnitool
./flannel
user@ubuntu-1:~/cni$
user@ubuntu-1:~/cni$ ls
bridge  cni-amd64-v0.4.0.tgz  cnitool  dhcp  flannel  host-local  ipvlan  loopback  macvlan  noop  ptp  tuning

如上所示

  • 我们首先创建了一个目录cni
  • 然后使用 curl 命令下载了 CNI 的归档文件,当使用 curl 下载文件时,我们需要传递 -O 参数来告诉 curl 保存为文件,使用 -L 参数来允许 curl 跟随重定向,因为我们下载的URL实际上会将我们30x重定向到其他URL。
  • 下载完成后,我们使用 tar 命令解压缩归档文件。

在完成这些操作后,我们可以看到有一些新文件解压出来了。现在,让我们聚焦在网桥插件bridge文件,Bridge 是 CNI 官方插件之一。您可能已经猜到,他的工作是将容器依附到网桥接口上。现在我们插件有了,接下来怎么使用它们呢?上面规范有一点提到,网络配置是通过STDIN流传输到插件中的,因此我们需要用STDIN将网络的配置信息输入到插件中,但这还不是插件所需的全部信息。插件还需要更多信息,比如您希望执行的操作,希望使用的命名空间以及其他各种信息。此信息通过环境变量传递到插件。没听懂?别担心,让我们来看一个例子。

首先,我们定义一个网桥的网络配置文件…

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

如上所示,我们为桥接网络创建了JSON格式的定义。上面列出了一些CNI的通用定义,以及一些bridge插件独有的定义,各个定义的作用如下

CNI通用参数

  • cniVersion:使用的CNI规范的版本
  • 名称:桥接网络名称
  • type:插件名称,在本例中,名称为 bridge 可执行文件的名称
  • args:可选的附加参数
  • ipMasq:是否为该网络配置出站地址转换(SNAT)
  • ipam
    • type:IPAM插件可执行文件的名称
    • subnet:要分配出的子网(实际上是IPAM插件的一部分)
    • routes
      • dst:您希望地址可达的子网
      • gw:到达目标的下一跳IP,如果未指定,则使用默认网关
  • DNS:
    • nameservers: 该网络的 DNS
    • domain:用于DNS请求的搜索域
    • search:搜索域列表
    • options:要传递的选项

Bridge 插件特有参数

  • isgateway:如果为true,则为网桥分配一个IP地址,以便连到网桥的容器可以将其用作网关。
  • isdefaultgateway:如果为true,则将分配的IP地址设置为默认路由。
  • forceAddress:如果先前的值已更改,则告诉插件分配一个新的IP。
  • mtu:定义网桥的MTU。
  • hairpinMode:为网桥上的接口设置发夹模式

上面粗体字的部分参数是我们在此示例中用到的。其他参数您也可以试下,了解他们的作用,但大部分看参数名就知道啥意思了。您可能注意到了,参数其中有一部分是 IPAM 插件的定义,但是关于 IPAM 我们不会在这篇文章中介绍(以后会!),我们只需要知道它是使用了多个 CNI 插件就行了。

好了,现在我们有了网络定义,我们要把它运行起来。然而,我们目前仅定义了网桥,而 CNI 的重点是配置容器网络,因此我们也需要告诉 CNI 插件要使用网络定义来配置容器,因为要通过传递环境变量给插件来操作,所以我们的命令可能看起来像这样...

sudo CNI_COMMAND=ADD CNI_CONTAINERID=1234567890 CNI_NETNS=/var/run/netns/1234567890 CNI_IFNAME=eth12 CNI_PATH=`pwd` ./bridge < mybridge.conf

让我们来看一下上面这条命令,我想大多数人可能都知道可以在shell或系统级别设置环境变量,但除此之外,您还可以把环境变量直接传递给命令,这样它们将仅由您正在调用的可执行文件使用,并且仅在执行过程中有效。因此,在这种情况下,以下变量将被传递给网桥可执行程序...

  • CNI_COMMAND = ADD: 告诉CNI要添加连接
  • CNI_CONTAINER = 1234567890:告诉CNI要使用的网络命名空间为“1234567890”
  • CNI_NETNS = /var/run/netns/1234567890:网络命名空间路径
  • CNI_IFNAME = eth12:我们希望命名空间里使用的网络接口名
  • **CNI_PATH = `pwd` **:我们需要告诉CNI插件的可执行文件在哪里。在这我们的实验中,由于我们已经在cni目录中,因此使用`pwd`命令(当前工作目录)。

在定义了要传递给可执行文件的环境变量后,接下来选择我们要使用的插件(在本案例中为bridge),然后我们使用<通过STDIN将网络定义传给插件。在运行命令之前,我们还需要创建插件将要配置的网络命名空间。通常容器运行时会自动创建命名空间,但由于我们是自己手动实验,所以首先我们得自己先创建一个网络命名空间...

sudo ip netns add 1234567890

创建完成后,让我们运行插件...

user@ubuntu-1:~/cni$ sudo CNI_COMMAND=ADD CNI_CONTAINERID=1234567890 CNI_NETNS=/var/run/netns/1234567890 CNI_IFNAME=eth12 CNI_PATH=`pwd` ./bridge < mybridge.conf
2017/02/17 09:46:01 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": {}
}user@ubuntu-1:~/cni$

执行完命令后返回了两部分输出

  • 首先由于 IPAM 找不到本地存储的保留IP分配信息文件,因此返回错误。如果我们对其他网络命名空间再次运行此命令,则不会出现此错误了,因为该文件在我们首次运行插件时创建了。
  • 其次是返回一个JSON格式的IP配置,在本例中,网桥本身配置为10.15.20.1/24的IP,而网络命名空间接口将会分配到10.15.20.2/24,它还设置了默认网关和我们在网络配置JSON中定义的1.1.1.1/32路由。

让我们看看它做了什么...

user@ubuntu-1:~/cni$ ifconfig
cni_bridge0 Link encap:Ethernet  HWaddr 0a:58:0a:0f:14:01
          inet addr:10.15.20.1  Bcast:0.0.0.0  Mask:255.255.255.0
          inet6 addr: fe80::3cd5:6cff:fef9:9066/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:8 errors:0 dropped:0 overruns:0 frame:0
          TX packets:8 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:536 (536.0 B)  TX bytes:648 (648.0 B)

ens32     Link encap:Ethernet  HWaddr 00:0c:29:3e:49:51
          inet addr:10.20.30.71  Bcast:10.20.30.255  Mask:255.255.255.0
          inet6 addr: fe80::20c:29ff:fe3e:4951/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:17431176 errors:0 dropped:1240 overruns:0 frame:0
          TX packets:14162993 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:2566654572 (2.5 GB)  TX bytes:9257712049 (9.2 GB)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:45887226 errors:0 dropped:0 overruns:0 frame:0
          TX packets:45887226 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1
          RX bytes:21016155576 (21.0 GB)  TX bytes:21016155576 (21.0 GB)

veth1fbfe91d Link encap:Ethernet  HWaddr 26:68:37:93:26:4a
          inet6 addr: fe80::2468:37ff:fe93:264a/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:8 errors:0 dropped:0 overruns:0 frame:0
          TX packets:16 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:648 (648.0 B)  TX bytes:1296 (1.2 KB)

user@ubuntu-1:~/cni$

注意,我们现在有了一个名为”cni_bridge0“的网桥接口,该接口 IP 和我们预期一致,注意在底部,有veth设备的一端。回想一下,我们还启用了ipMasq,如果我们查看主机的iptables,将看到如下规则...

user@ubuntu-1:~/cni$ sudo iptables-save | grep mybridge
-A POSTROUTING -s 10.15.20.0/24 -m comment --comment "name: \"mybridge\" id: \"1234567890\"" -j CNI-26633426ea992aa1f0477097
-A CNI-26633426ea992aa1f0477097 -d 10.15.20.0/24 -m comment --comment "name: \"mybridge\" id: \"1234567890\" -j ACCEPT
-A CNI-26633426ea992aa1f0477097 ! -d 224.0.0.0/4 -m comment --comment "name: \"mybridge\" id: \"1234567890\"" -j MASQUERADE
user@ubuntu-1:~/cni$

再让我们看一下网络命名空间...

user@ubuntu-1:~/cni$ sudo ip netns exec 1234567890 ifconfig
eth12     Link encap:Ethernet  HWaddr 0a:58:0a:0f:14:02
          inet addr:10.15.20.2  Bcast:0.0.0.0  Mask:255.255.255.0
          inet6 addr: fe80::d861:8ff:fe46:33ac/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:16 errors:0 dropped:0 overruns:0 frame:0
          TX packets:8 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:1296 (1.2 KB)  TX bytes:648 (648.0 B)

user@ubuntu-1:~/cni$ sudo ip netns exec 1234567890 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.2
user@ubuntu-1:~/cni$

我们的网络命名空间配置也如预期所示,命名空间有一个名为”eth12”的网络接口,其IP地址为10.15.20.2/24,我们之前定义的路由也在那里,至此大功告成!

这只是一个简单的示例,但我认为它阐述了CNI的实现和工作方式。下周,我们将研究一个如何在容器运行时中使用CNI的示例,进一步研究CNI插件。

在总结之前,我想简单说下我最初卡壳的一个地方——调用插件的方式。在我们的示例中,我们命令中./bridge直接调用了一个特定插件,因此,我一开始对为什么还需要使用“CNI_PATH”环境变量指定插件的位置感到困惑,显然我们已经了知道插件的路径。但其实真正原因是手动调用插件这不是通常使用CNI的方式,通常会有另一个应用程序或系统会读取CNI的网络定义并运行,在种情况下,“CNI_PATH"会在系统内定义好。由于网络配置文件定义了要使用的插件(在我们的案例中为bridge),因此所有系统都需要知道在哪里可以找到插件。为了找到插件,系统们会引用CNI_PATH变量。我们将在以后的文章中讨论这个问题,我们将讨论其他应用程序使用CNI(cough,Kubernetes)的原因.到目前为止,我们只知道上面示例的CNI的工作原理,但没有演示一些实际情况的典型用例。