本篇主要是简单了解CNI,代码链接在此

概览

CNI(Container Network Interface)是用来配置Linux容器中的网络接口,它包含了一套规范和一些列的库文件,可以通过各种插件实现所需要的功能。

首先是规范和库文件,他是为了使所有的插件都能统一API,保证了插件的规范性。
接下来就是插件了,这个一般是我们最关注的,能够根据我们的需求实现容器网络的管理。官方文档列出了一系列的第三方插件,实现了不少场景下的容器网络管理,这里我们不列出了,有兴趣可以自己去看。

规范

规范文档在此

我们以版本0.4.0为准熟悉一下。该版本引入了CHECK命令和在DEL命令中添加prevResult的参数传入。

有以下几点需要注意(我比较关注的):

  • 不能为同一容器调用并行操作,但允许为不同的容器调用并行操作。
  • 必须要有ADD和DEL操作,ADD总是最后跟随相应的DEL。 DEL可以跟随其他DEL,但插件应该允许多个DEL。
  • ContainerID唯一标识。存储状态的插件应使用关键字(网络名称,CNI_CONTAINERID,CNI_IFNAME)执行此操作。
  • 对于相同的(网络名称,容器ID,容器内的接口名称),不能再调用ADD两次(没有相应的DEL)。
  • 除了标记可选的,CNI结构中的字段(如网络配置和CNI插件结果)是必需的。

CNI插件

CNI插件都必须是可执行文件,它负责将网络接口插入容器namespace中并在主机上进行一些操作(比如将veth pair的一端插入namespace,并且将另一端连接到bridge),然后调用IPAM插件将IP分配给容器接口,并且配置IP地址相关的路由等信息。

插件参数

之前说过必须支持的操作有ADD和DEL,接下来看一下ADD和DEL分别支持的参数。

  • ADD:给容器添加网络

    • 参数:

      • Container ID,运行容器时分配的唯一明文ID,不能为空。
      • Network namespace path,表示要添加的容器的网络namespace路径,比如/proc/$pid/ns/net或者bind-mount/link到namespace路径的。
      • Network configuration,这是一个json格式的文档,描述网络信息。
      • Extra arguments,提供了替代机制,可以为每个容器进行简单配置。
      • Name of the interface inside the container,容器中的网口名称。
    • Result

      • Interfaces list,容器内的端口和宿主机的端口列表以及端口的硬件地址等信息
      • IP configuration assigned to each interface,容器和宿主机的端口上配置的IP地址,网关和路由信息。
      • DNS information,nameservers、domain、search domains等DNS信息
  • DEL:删除网络

    • 参数

      • Container ID,同上。
      • Network namespace path,同上。
      • Network configuration,同上。
      • Extra arguments,同上。
      • Name of the interface inside the container,同上。
    • DEL操作的所有参数都必须与ADD相同。
    • DEL操作应该释放containerid配置的所有网络资源。
    • 如果有一个已知的ADD操作,那么执行DEL操作时,必须将之前ADD的返回值放入prevResult的字段,期望使用libcni支持缓存Result。
    • 当没有提供CNI_NETNSprevResult的话,插件应该尽量清除所有的资源(比如释放IPAM的所有申请资源)并且返回成功。
    • 如果缓存了之前ADD操作的返回值Result,那么执行DEL操作时也要删除缓存的Result。

即使缺少一些资源,插件通常也应该完成DEL操作,而不会出错。
例如,IPAM插件通常应该释放IP并返回成功,即使容器namespace不存在。
再举一个例子,即使容器namespace或容器网络接口不存在,bridge插件也应该将DEL操作委托给IPAM插件并清理自己的资源。

接下来再看一下可选项CHECK和VERSION

  • CHECK:检测容器的网络配置是否达预期

    • 参数

      • Container ID,和ADD一致。
      • Network namespace path,和ADD一致。
      • Network configuration,和ADD一致。必须要有prevResult字段。
      • Extra arguments,和ADD一致。
      • Name of the interface inside the container,和ADD一致。
    • Result

      • 插件无返回或者返回错误。
    • 插件比如要有prevResult字段来确定端口和地址是否符合预期。
    • 插件比如允许后续的插件来修改网络资源,比如route。
    • 如果一个资源包含了如下CNI的返回类型(interface, address or route),那么插件应该返回一个错误

      • 已经被插件创建了
      • 已经在prevResult
      • 不存在或者无效状态
    • 如果其他资源没有检测到如下的Result类型,或者处于无效状态,那么插件也应该返回一个错误

      • Firewall rules
      • Traffic shaping controls
      • IP reservations
      • External dependencies such as a daemon required for connectivity
    • 如果插件知道容器无法访问的情况,则应该返回错误。
    • 插件必须处理在ADD之后立即调用的CHECK,因此应该允许任何异步资源的合理收敛延迟。
    • 该插件应该在任何委派的(例如IPAM)插件上调用CHECK,并将任何错误传递给其调用者。
    • 运行时不能为尚未添加ADD的容器调用CHECK,或者在上一次ADD之后已经被DEL。
    • 如果在配置列表中将disableCheck设置为true,则运行时不能调用。
  • VERSION: Report version

    • Parameters: NONE
    • Result: CNI当前的版本以及支持的版本。

运行时必须使用网络类型作为要调用的可执行文件的名称(比如bridge或者sriov)。运行时应在预定义目录列表中查找此可执行文件。一旦找到,它必须使用以下环境变量调用可执行文件以进行参数传递:

  • CNI_COMMAND:表示所需的操作:ADD,DEL,CHECK或VERSION。
  • CNI_CONTAINERID:容器ID。
  • CNI_NETNS:网络命名空间文件的路径。
  • CNI_IFNAME:要设置的接口名称,如果插件无法使用此接口名称,则必须返回错误。
  • CNI_ARGS:用户在调用时传入的额外参数。由分号分隔的key-value,例如"FOO = BAR; ABC = 123"。
  • CNI_PATH:搜索CNI插件可执行文件的路径列表。路径由特定于OS的列表分隔符分隔,例如Linux上的':'和Windows上的';'。

必须通过stdin将JSON格式的网络配置流式传输到插件。这意味着它不依赖于磁盘上的特定文件,并且可能包含在调用之间更改的信息。

返回值

请注意,IPAM插件应返回缩写的Result结构,如IP分配中所述。

插件必须指示成功,返回代码为零,并且在ADD命令的情况下,将以下JSON打印到stdout。 ips和dns项应该与IPAM插件返回的输出相同,但插件应适当填写接口索引,因为IPAM插件应该不知道IPAM插件输出接口。

{
  "cniVersion": "0.4.0",
  "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": {                                                   (optional)
    "nameservers": <list-of-nameservers>                     (optional)
    "domain": <name-of-local-domain>                         (optional)
    "search": <list-of-additional-search-domains>            (optional)
    "options": <list-of-options>                             (optional)
  }
}

cniVersion指定插件使用的CNI版本。 插件可能支持多个CNI规范版本(因为它通过VERSION命令报告),此处插件返回的结果中的cniVersion必须与网络配置中指定的cniVersion一致。 如果插件不支持网络配置中的cniVersion,则插件应返回错误代码1。

interfaces描述了插件创建的特定网络接口。 如果存在CNI_IFNAME变量,则插件必须使用该名称作为容器/管理程序接口,否则返回错误。

  • mac:接口的硬件地址。如果L2地址对插件没有意义,则此字段是可选的。
  • sandbox:基于容器/namespace的环境应该返回该sandbox的网络命名空间的完整文件系统路径。必须为创建或移动到sandbox中的接口提供此项。

ips字段是IP配置信息的列表。

routes字段是路由配置信息的列表。

dns字段包含由公共DNS信息组成的字典。

错误必须由非零返回码表示,并且以下JSON将打印到stdout:

{
  "cniVersion": "0.4.0",
  "code": <numeric-error-code>,
  "msg": <short-error-message>,
  "details": <long-error-message> (optional)
}

Error 0-99 是预留的错误类型,100以后是可以自定义的。

网络配置

网络配置用json格式描述。配置可以存储在磁盘上,也可以由容器运行时从其他源生成。以下是字段介绍:

  • cniVersion:CNI规范的版本。
  • name:网络名称。应该是唯一的。
  • type:CNI插件可执行文件的文件名。
  • args(可选):容器运行时提供的附加参数。
  • ipMasq(可选):如果插件支持,则在该主机的主机上设置IP伪装。如果主机将充当到无法路由到分配给容器的IP的子网的网关,则必须执行此操作。
  • ipam(可选):具有IPAM特定值的字典。

    • type:指IPAM插件可执行文件的文件名。
  • dns(可选):具有DNS特定值的字典。

    • nameservers(可选):此网络知道的DNS名称服务器的优先级排序列表。列表中的每个条目都是包含IPv4或IPv6地址。
    • domain(可选):用于短主机名查找的本地域。
    • search(可选):用于短主机名查找的优先级排序搜索域列表。大多数解析器都会优先考虑域名。
    • options(可选):可以传递给解析程序的选项列表

插件可以定义它们接受的其他字段,如果使用未知字段调用,则可能会生成错误。例外情况是args字段可用于传递任意数据,如果不识别则忽略。

{
  "cniVersion": "0.4.0",
  "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"
    }
  }
}

网络配置列表

网络配置列表提供了一种机制,可以按照定义的顺序为单个容器运行多个CNI插件,并将每个插件的结果传递给下一个插件。

该列表以JSON格式描述,可以存储在磁盘上,也可以由容器运行时从其他源生成。字段介绍如下:

  • cniVersion:此配置列表和所有单个配置符合的CNI规范版本。
  • name:网络名称,应该是唯一的。
  • disableCheck:true or false。如果disableCheck为true,则运行时不得为此网络配置列表调用CHECK。
  • plugins:标准CNI网络配置字典列表。

对于ADD操作,还必须在第一个插件之后的任何插件的配置JSON中添加prevResult字段,该插件必须是JSON格式的前一个插件的Result。
对于CHECK和DEL操作,必须将prevResult字段添加到每个插件的配置JSON中,它必须是JSON格式的前一个ADD操作的结果。
对于ADD操作,插件应该将prevResult字段的内容回显到它们的stdout,以允许后续插件接收结果,除非他们希望修改或抑制先前的结果。允许插件修改或禁止全部或部分prevResult。但是,支持包含prevResult字段的CNI规范版本的插件必须通过传递,修改或明确禁止它来处理prevResult。
对于DEL操作,运行时必须以反向顺序执行插件。

当在插件列表上执行操作时发生错误时,必须停止执行列表。

如果ADD操作失败,当运行时决定处理失败时,它应该对列表中的所有插件执行DEL操作(与上面指定的ADD相反的顺序),即使在ADD操作期间未调用某些插件也是如此。

接下来查看一个示例,首先调用tuning的插件,里面包含了ADD操作返回的prevResult字段。然后调用bridge的插件,里面也包含了ADD操作返回的prevResult字段。

{
  "cniVersion": "0.4.0",
  "name": "dbnet",
  "type": "tuning",
  "sysctl": {
    "net.core.somaxconn": "500"
  },
  "prevResult": {
    "ips": [
        {
          "version": "4",
          "address": "10.0.0.5/32",
          "interface": 2
        }
    ],
    "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" ]
    }
  }
}
{
  "cniVersion": "0.4.0",
  "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" ]
  },
  "prevResult": {
    "ips": [
        {
          "version": "4",
          "address": "10.0.0.5/32",
          "interface": 2
        }
    ],
    "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" ]
    }
  }
}

IP申请

CNI插件为接口分配(和维护)IP地址,然后配置与该接口相关的路由。这为CNI插件提供了极大的灵活性,但也给它带来了很大的负担。许多CNI插件需要具有相同的代码以支持用户想要的若干IP管理方案(例如,dhcp,host-local)。

为了减轻负担并使IP管理策略,我们定义了第二种类型的插件--IP地址管理插件(IPAM)。CNI插件的责任是在适当时刻调用IPAM插件。 IPAM插件必须确定接口IP/子网,网关和路由,并将此信息返回到main插件。 IPAM插件可以通过协议(例如dhcp),存储在本地文件系统上的数据,网络配置文件的ipam部分或上述的组合来获得信息。

与CNI插件一样,通过可执行文件来调用IPAM插件。 通过CNI_PATH指示CNI插件。 IPAM插件必须接收传递给CNI插件的所有相同环境变量。 就像CNI插件一样,IPAM插件通过stdin接收网络配置。

必须通过零返回代码指示成功,并将以下JSON打印到stdout(在ADD命令的情况下):

{
  "cniVersion": "0.4.0",
  "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": {                                          (optional)
    "nameservers": <list-of-nameservers>            (optional)
    "domain": <name-of-local-domain>                (optional)
    "search": <list-of-search-domains>              (optional)
    "options": <list-of-options>                    (optional)
  }
}

请注意,IPAM插件应返回不包含interfaces字段的Result结构,因为IPAM插件应该不知道其父插件配置的接口,除了IPAM特别需要的接口(例如,像dhcp IPAM插件) )。

IPAM插件示例:

  • host-local:在指定范围内选择未使用的(由同一主机上的其他容器)IP。
  • dhcp:使用DHCP协议获取和维护租约。 DHCP请求将通过创建的容器接口发送;因此,相关网络必须支持广播。

一些通用数据结构

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配置信息的列表。

version:对应于条目中IP地址的IP地址。
address:CIDR表示法中的IP地址(例如192.168.1.3/24)。
gateway:此子网的默认网关。它不指示CNI插件使用此网关添加任何路由:要添加的路由通过routes字段单独指定。使用此值的一个示例是CNI网桥插件将此IP地址添加到Linux网桥以使其成为网关。
interface:CNI插件的接口列表索引结果,指示应该将此IP配置应用于哪个接口。 IPAM插件不应返回此密钥,因为它们没有关于网络接口的信息。

Routes

  "routes": [
      {
          "dst": "<ip-and-prefix-in-cidr>",
          "gw": "<ip-of-next-hop>"               (optional)
      },
      ...
  ]

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

nameservers:此网络知道的DNS名称服务器的优先级排序列表。 列表中的每个条目都是包含IPv4或IPv6地址的字符串。
domain:用于短主机名查找的本地域。
search:用于短主机名查找的优先级排序搜索域列表。 大多数解析器都会优先考虑域名。
options:可以传递给解析程序的选项列表。

常见错误码

除此处指定外,不得使用错误代码1-99。

1 - 不兼容的CNI版本
2 - 网络配置中不支持的字段。 错误消息必须包含不受支持的字段的key和value。
3 - 容器未知或不存在。 此错误意味着运行时不需要执行任何容器网络清理(例如,在容器上调用DEL操作)。
11 - 稍后再试。 如果插件检测到应该清除的某些瞬态条件,它可以使用此代码通知运行时它应该稍后重新尝试操作。

库文件

使用

CNI规范与语言无关,但是目前实现中使用了Golang,参考插件我们后续再看。

我们这里跳过安装cni和插件,直接看看怎么使用:

首先我们要有插件对应的配置文件

cat >/etc/cni/net.d/10-mynet.conf <<EOF
{
    "cniVersion": "0.2.0",          //cni规范版本
    "name": "mynet",                //插件名称
    "type": "bridge",               //类型
    "bridge": "cni0",               //bridge名称
    "isGateway": true,              //是否网关
    "ipMasq": true,                 //防火墙
    "ipam": {
        "type": "host-local",       //ipam类型
        "subnet": "10.22.0.0/16",   //子网
        "routes": [
            { "dst": "0.0.0.0/0" }  //网关
        ]
    }
}

接下来编译插件

$ cd $GOPATH/src/github.com/containernetworking/plugins
$ ./build_linux.sh # or build_windows.sh

最后在该网络中运行ifconfig的操作,其中用到的脚本可以看下源码,没有难度。

$ CNI_PATH=$GOPATH/src/github.com/containernetworking/plugins/bin
$ cd $GOPATH/src/github.com/containernetworking/cni/scripts
$ sudo CNI_PATH=$CNI_PATH ./priv-net-run.sh ifconfig
eth0      Link encap:Ethernet  HWaddr f2:c2:6f:54:b8:2b
          inet addr:10.22.0.2  Bcast:0.0.0.0  Mask:255.255.0.0
          inet6 addr: fe80::f0c2:6fff:fe54:b82b/64 Scope:Link
          UP BROADCAST MULTICAST  MTU:1500  Metric:1
          RX packets:1 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:1 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:90 (90.0 B)  TX bytes:0 (0.0 B)

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:0 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:0 (0.0 B)  TX bytes:0 (0.0 B)

在容器中运行ifconfig的操作。

$ CNI_PATH=$GOPATH/src/github.com/containernetworking/plugins/bin
$ cd $GOPATH/src/github.com/containernetworking/cni/scripts
$ sudo CNI_PATH=$CNI_PATH ./docker-run.sh --rm busybox:latest ifconfig
eth0      Link encap:Ethernet  HWaddr fa:60:70:aa:07:d1
          inet addr:10.22.0.2  Bcast:0.0.0.0  Mask:255.255.0.0
          inet6 addr: fe80::f860:70ff:feaa:7d1/64 Scope:Link
          UP BROADCAST MULTICAST  MTU:1500  Metric:1
          RX packets:1 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:1 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:90 (90.0 B)  TX bytes:0 (0.0 B)

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:0 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:0 (0.0 B)  TX bytes:0 (0.0 B)

CNI插件以后的规划

  • 动态更新网络配置
  • 网络带宽和防火墙规则的动态策略
最后修改:2021 年 08 月 18 日
如果觉得我的文章对你有用,请随意赞赏