Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

CNI学习笔记

1
Feb
2020

CNI学习笔记

By Alex
/ in PaaS
/ tags CNI, K8S
0 Comments
简介
容器网络

Kubernetes没有提供默认可用的容器网络,第三方提供的容器网络,必须满足以下条件:

  1. 容器之间可以相互通信,且不需要NAT
  2. 宿主机和容器可以相互通信,且不需要NAT
  3. 容器看到自己的IP,和其它节点/容器看到的它的IP,是一样的

即集群包含的每一个容器都拥有一个与集群中其它的容器、节点可直接路由的独立IP地址。但是Kubernetes并没有具体实现这样一个网络模型,而是设计了一个开放的容器网络标准CNI。

K8S容器网络,具有两种实现风格:

  1. Overlay Network,即通用的虚拟化网络模型,不依赖于宿主机底层网络架构,可以适应任何的应用场景,方便快速体验。但是性能较差,因为在原有网络的基础上叠加了一层Overlay网络,封包解包或者NAT对网络性能都是有一定损耗的
  2. Underlay Network,即基于宿主机物理网络环境的模型,容器与现有网络可以直接互通,不需要经过封包解包或是NAT,其性能最好。但是其普适性较差,且受宿主机网络架构的制约,比如MAC地址可能不够用
网络插件

K8S的网络插件主要支持两类:

  1. CNI插件:基于v0.4.0版本,本文的主题
  2. Kubenet插件:没什么用,通过网桥和host-local CNI插件简单的实现cbr0

Kubelet使用单个默认网络插件,全集群使用一个默认网络。在Kubelet启动时,它会探测插件列表,并且记住它们,在Pod生命周期的适当阶段(仅对于Docker,CRI会自行管理CNI插件)调用选择的插件。两个相关的命令行选项:

--cni-bin-dir  Kubelet启动时从该目录中探测插件
--network-plugin  对于CNI插件,取值 cni

除了实现NetworkPlugin接口:

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
type NetworkPlugin interface {
    // Init initializes the plugin.  This will be called exactly once
    // before any other methods are called.
    Init(host Host, hairpinMode kubeletconfig.HairpinMode, nonMasqueradeCIDR string, mtu int) error
 
    // Called on various events like:
    // NET_PLUGIN_EVENT_POD_CIDR_CHANGE
    Event(name string, details map[string]interface{})
 
    // Name returns the plugin's name. This will be used when searching
    // for a plugin by name, e.g.
    Name() string
 
    // Returns a set of NET_PLUGIN_CAPABILITY_*
    Capabilities() utilsets.Int
 
    // SetUpPod is the method called after the infra container of
    // the pod has been created but before the other containers of the
    // pod are launched.
    SetUpPod(namespace string, name string, podSandboxID kubecontainer.ContainerID, annotations, options map[string]string) error
 
    // TearDownPod is the method called before a pod's infra container will be deleted
    TearDownPod(namespace string, name string, podSandboxID kubecontainer.ContainerID) error
 
    // GetPodNetworkStatus is the method called to obtain the ipv4 or ipv6 addresses of the container
    GetPodNetworkStatus(namespace string, name string, podSandboxID kubecontainer.ContainerID) (*PodNetworkStatus, error)
 
    // Status returns error if the network plugin is in error state
    Status() error
} 

以创建/清理容器网络,网络插件还需要支持kube-proxy。对于iptables模式的kube-proxy来说,对iptables的依赖是很显然的,因此网络插件应当进行适当的配置,让容器流量对宿主机的iptabels可见。例如,当容器连接到Linux bridge时,必须设置/net/bridge/bridge-nf-call-iptables为1以确保iptables proxy正常工作。使用其它技术时,你同样需要考虑为kube-proxy设置正确的路由。

使用CNI插件

在K8S中,如果设置--network-plugin=cni则选用CNI作为网络插件。这时Kubelet会读取--cni-conf-dir=/etc/cni/net.d中的CNI配置,来创建Pod网络。配置文件中引用的任何插件都必须存在于--cni-bin-dir=/opt/cni/bin目录下。

如果--cni-bin-dir下有多个插件配置,则Kubelet使用按字典序排名最靠前的那个配置文件。

从CNI 0.2.0开始,除了通过配置文件指定的CNI插件,Kubelet还会使用一个标准的CNI插件lo,它不需要配置。

CNI项目

容器网络接口(Container Network Interface,CNI)是CNCF项目之一,它关注容器的网络连接,负责在容器启动时创建网络资源,在容器删除时清理为其创建的网络资源。由于网络和运行环境关系很密切,因此很必要进行插件化,这是创建CNI项目的缘由。

CNI项目包含两大部分:

  1. CNI规范文档:
    1. libcni,Go语言的CNI接口,供运行时调用,转调具体CNI插件
    2. skel,Go语言的CNI插件骨架
    3. https://github.com/containernetworking/cni
  2. 一系列参考实现和样例插件:
    1. 接口插件:ptp、bridge、macvlan……
    2. Chained插件:portmap、bandwidth、tuning
    3. https://github.com/containernetworking/plugins
CNI规范

具有以下特点:

  1. 供应商中立,不仅仅是为K8S设计。还可以被Mesos、CloudFoundry、podman、CRI-O使用
  2. 为网络操作定义了基本的执行流、配置格式
  3. 简单、向后兼容
配置格式

JSON格式,对于任何操作,此配置都从标准输入喂给插件。可以包含标准的配置项、插件特有的配置项,示例:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
{
  "name": "mynet",
  "type": "bridge",
  "bridge": "mynet0",
  "isDefaultGateway": true,
  "forceAddress": false,
  "ipMasq": true,
  "hairpinMode": true,
  "ipam": {
    "type": "host-local",
    "subnet": "10.10.0.0/16"
  }
}
运行方式
  1. CNI插件是可执行文件,支持ADD / DEL / CHECK / VERSION几个命令
  2. 当期望进行网络配置操作时,由容器运行时调用并产生CNI插件进程
  3. JSON配置、容器相关的数据,通过stdin传递给CNI插件
  4. CNI插件通过stdout报告处理结果 
未来方向

在未来,CNI可能会支持:

  1. 动态更新现有网络配置
  2. 网络带宽和防火墙策略的动态策略支持
知名实现
CNI实现 说明
calico 参考:基于Calico的CNI
galaxy 参考:Galaxy学习笔记
flannel 参考:Flannel学习笔记
规范
版本

规范的版本和CNI库的版本、插件样例/参考实现的版本无关。

多版本支持

为了支持平滑升级,CNI插件的作者应当兼容多版本的CNI规范。特别是,已经发布出去的插件,需要保持对老版本的兼容。插件应该在应答 VERSION命令时,返回类似下面的结构:

JSON
1
2
3
4
{
  "cniVersion": "0.3.0",
  "supportedVersions": [ "0.1.0", "0.2.0", "0.3.0" ]
}

对于ADD命令,插件必须尊重网络配置JSON中指定的cniVersion:

  1. 如果网络配置没有提供cniVersion字段,假设使用v0.2.0版本,返回结果应该是v0.2.0格式的
  2. 如果插件不支持配置指定的版本,应该返回错误
v0.1.0

初始版本。

ADD命令正常结果格式:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
  "cniVersion": "0.1.0",
  "ip4": {
    "ip": <ipv4-and-subnet-in-CIDR>,
    "gateway": <ipv4-of-the-gateway>,  (optional)
    "routes": <list-of-ipv4-routes>    (optional)
  },
  "ip6": {
    "ip": <ipv6-and-subnet-in-CIDR>,
    "gateway": <ipv6-of-the-gateway>,  (optional)
    "routes": <list-of-ipv6-routes>    (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)
  }
}

出错时的结果格式:

JSON
1
2
3
4
5
6
{
  "cniVersion": "0.1.0",
  "code": <numeric-error-code>,
  "msg": <short-error-message>,
  "details": <long-error-message> (optional)
}
v0.2.0

该版本主要增加了VERSION命令。该命令的返回结果格式参考上文。

该版本中args被作为一个保留字段。这样可以提供任意的结构化信息,而不是仅仅靠CNI_ARGS传递 k1=v1;k2=v2形式的字符串。

v0.3.0

此版本提供了更加丰富的关于容器网络配置的信息,包括网络接口的细节信息,并且支持多个IP地址。

ADD命令正常结果格式:

JSON
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.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)
      }
  ],
  "ip": [
      {
          "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)
  }
}

可以看到,和容器有关的网络接口的详细信息包含在结果中,每个接口都可以包含多个ip信息。

v0.3.1

0.3.0版本存在一个BUG,Result结构中的ip字段应当已经被重命名为ips,以保证和IPAM的Result结构一致。v0.3.1解决此问题,并且所有first-party CNI插件(bridge,host-local等)都已经在0.3.1修改为,使用ips字段。

所有v.0.3.0的CNI插件需要注意此差异。

v0.4.0

该版本主要增加了CHECK命令。

总览

CNI规范来自rkt网络提案,它规定了Linux下应用容器网络的插件化解决方案。在此规范中,术语:

  1. 容器(container)可以认为是Linux网络命名空间的同义词,其关联的单元取决于容器运行时,对于K8S来说是Pod,对于Docker来说是一个裸容器
  2. 网络(network)是一组可被唯一寻址的实体,它们能够相互通信。这些实体可以是上一条提到的容器、一个主机、某些网络设备(例如路由器)。容器可以被添加到1-N个网络,或者从中删除

CNI规范规定的是运行时和插件之间的接口。 

一般性建议
  1. 在调用任何插件之前,容器运行时必须先为容器创建网络命名空间 
  2. 运行时必须确定,容器属于哪些网络,对于这些网络,分别应当调用什么插件
  3. 配置文件格式为JSON,包含name、type等必须字段。每次插件调用,参数可以不一样,为此目的,可选的 args字段包含变化的信息
  4. 为了将容器添加到每个网络,运行时必须逐个的为每个网络调用插件
  5. 在容器销毁时,运行时必须以相反顺序调用插件,将容器从网络断开
  6. 运行时可以并行的为多个容器调用插件,但是对于单个容器,必须串行调用
  7. 容器必须有唯一性的标识ContainerID,如果需要存储状态,插件必须以网络名、CNI_CONTAINERID、CNI_IFNAME(容器内接口名)作为联合主键
  8. 对于任一个上述联合主键,ADD不得被运行时调用两次。这隐含表示:一个容器不得被加入同一网络两次,除非它具有两个网络接口
CNI接口

插件必须实现为可执行文件,rkt/K8S等容器管理系统将通过命令行来调用它们。运行时配置(RuntimeConf)一般通过环境变量传递,网络配置(NetworkConfig)则是通过标准输入喂入。

CNI插件负责把一个网络接口(例如VETH对的一端)插入到容器的网络命名空间,并在宿主机上执行任何必要的操作(例如将VETH对的另外一端接到网桥)。CNI插件还必须给接口分配IP地址,并且通过调用适当的IPAM插件,创建和IPAM一致的路由规则。

ADD

该接口负责将容器添加到网络中。

参数:

  1. Container ID,容器唯一标识符文本,由运行时分配,不得为空。以数字/字母开头,可以包含 _.-字符
  2. Network namespace path,网络命名空间的路径,形式 /proc/PID/ns/net,或者指向该文件的绑定挂载/链接
  3. Network configuration,描述网络的JSON配置
  4. Extra arguments,基于容器级别的定制化配置参数
  5. Name of the interface inside the container,在容器网络命名空间中创建并分配的网络接口的名字,符合Linux接口命名空间,不得超过16字符,不含 / :或空白字符

结果:

  1. Interfaces list,取决于插件,返回的信息可能包括沙盒(容器或Hypervisor)接口名、宿主机接口名、接口的硬件地址、沙盒的详细信息
  2. IP configuration assigned to each interface,分配到沙盒/宿主机的IPv4/IPv6地址、网关、路由
  3. DNS information,包含DNS信息(服务器、domain、search domains、options)的字典
DEL

该接口负责将容器从网络中移除。

参数:和ADD相同。

注意点:

  1. 所有参数必须和ADD时一致
  2. DEL必须清理掉容器持有的、在目标网络中的所有资源
  3. 如果存在已知的,针对容器的先前ADD操作,则运行时必须在插件(or all plugins in a chain)JSON中添加prevResult字段,该字段的内容是上一个ADD操作的结果,JSON形式
  4. 如果CNI_NETNS或/和prevResult没有提供,则插件应当尽可能清理更多的资源(例如释放IPAM分配的地址)并返回提示操作成功的响应
  5. 如果运行时缓存了针对容器的上一个ADD的结果,则它应该在成功DEL后清理掉此缓存

如果某些资源丢失,此接口一般也不返回错误。例如:

  1. 即使容器网络命名空间已不存在,IPAM插件也应该释放IP地址并且返回成功,除非网络命名空间对于IPAM管理是关键的
  2. bridge插件应当将DEL委托给IPAM插件,并清理自身的资源,即使网络命名空间/容器接口已不存在
VERSION

该接口报告CNI插件相关的版本信息。没有参数,结果示例:

JSON
1
2
3
4
5
6
{
  // 当前CNI版本
  "cniVersion": "0.4.0",
  // 支持的CNI版本
  "supportedVersions": [ "0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0" ]
}
CHECK

检查容器的网络是否如预期一样。

参数:和ADD一样, 但是Network configuration包含prevResult字段,存放上一个ADD调用的结果。

结果:返回错误或空。

注意点:

  1. 插件必须检查prevResult,确定接口、地址符合预期
  2. 插件必须允许后续的chained plugin看到修改后的网络资源,例如路由
  3. 如果prevResult中列出的资源(接口、路由、地址)不存在,或者状态异常,应该返回错误
  4. 同样的,没有在prevResult中跟踪的网络资源,例如防火墙规则、流量塑形控制规则、IP预留、外部依赖,不存在或状态异常时,也应该返回错误
  5. 如果发现容器不可达,应单返回错误
  6. 插件需要处理在ADD后紧跟着的CHECK调用,这意味着需要考虑某些异步资源的合理创建延迟
  7. 插件需要调用所有被委托插件(例如IPAM)的CHECK,并将错误收集并返回
  8. 运行时不会对尚未ADD的容器调用CHECK,也不会对在ADD后进行DEL的容器进行CHECK
  9. 如果在配置列表中,disableCheck设置为true,则运行时不会调用CHECK
  10. 如果chain中一个插件返回错误,运行时可能选择停止继续迭代后续插件的CHECK
  11. 在ADD调用之后,直到DEL调用之前,运行时都可以调用CHECK
  12. 运行时可以假设CHECK失败的容器处于不可恢复的错误配置状态中
CNI调用结果

如果ADD操作成功,插件应该以0退出,并且在标准输出上打印JSON。JSON中ips、dns部分必须和IPAM插件输出的一致,interfaces数组则需要插件填写,因为IPAM插件对接口无感知:

JSON
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
{
  // 必须和Network Configuration中的一致
  "cniVersion": "0.4.0",
  // IPAM插件忽略此键值
  // 插件创建的接口列表
  // 如果变量CNI_IFNAME存在,插件必须使用此名字来创建sandbox/hypervisor接口,如果不能,返回错误
  "interfaces": [
      {
          "name": "<name>",
          // 如果L2地址有价值,则必须
          "mac": "<MAC address>",
          // 对于容器接口,必须是网络命名空间路径
          // 对于虚拟机,必须是虚拟化沙盒的ID,网络接口在其中创建
          // 对于宿主机接口,为空或忽略
          "sandbox": "<netns path or hypervisor identifier>"
      }
  ],
  // IP配置信息
  "ips": [
      {
          "version": "<4-or-6>",
          "address": "<ip-and-prefix-in-CIDR>",
          "gateway": "<ip-address-of-the-gateway>", // 可选
          "interface": <numeric-index-into-interfaces-list>
      },
      ...
  ],
  // 路由配置信息
  "routes": [ // 可选
      {
          "dst": "<ip-and-prefix-in-cidr>",
          "gw": "<ip-of-next-hop>"                           // 可选
      },
      ...
  ],
  // DNS信息
  "dns": { // 可选
    "nameservers": <list-of-nameservers>
    "domain": <name-of-local-domain>
    "search": <list-of-additional-search-domains>
    "options": <list-of-options>
  }
}

如果ADD操作失败,则应该以非0退出,并打印: 

JSON
1
2
3
4
5
6
{
  "cniVersion": "0.4.0",
  "code": <numeric-error-code>,
  "msg": <short-error-message>,
  "details": <long-error-message> (optional)
}
网络配置

网络配置为JSON格式,可以存放在磁盘上,或者由运行时从其它源动态生成。

插件可以定义自己的字段,知名字段包括:

字段 说明
cniVersion 字符串。当前配置遵循的CNI规范版本
name 字符串。网络名,在主机范围内必须唯一
type 字符串。CNI插件的可执行文件的名字
args

字典,可选。由运行时提供的额外参数。例如可以传递一组标签

此字段可以传入任何值

ipMasq 布尔,可选。如果插件支持,则在宿主机上为此网络设置IP遮掩(动态SNAT)。如果宿主机需要充当容器IP的网关,则为true
ipam

字典,可选。IPAM相关的配置:

type:IPAM插件的可执行文件名

dns

字典,可选。DNS相关配置:

nameservers:此网络可以感知的,优先级排序的DNS服务器列表
domain:用于短名查找的本地域后缀
search:字符串列表,优先级排序的短名查找DNS后缀
options:传递给resolver的选项

runtimeConfig

知名非标准字段。运行时动态填充的信息应该存放在此

通过列出capabilities,插件可以请求运行时填充必要的动态信息

capabilities

知名非标准字段。和runtimeConfig配合使用,提示运行时插件具有哪些特性,运行时因而能够提供这些特性所需的动态参数

例如,一个端口映射插件,可以这样配置:

JSON
1
2
3
4
5
{
  "name" : "ExamplePlugin",
  "type" : "port-mapper",
  "capabilities": {"portMappings": true}
}

这样,运行时传递给插件的、填充后的网络配置可能如下:

JSON
1
2
3
4
5
6
7
8
9
{
  "name" : "ExamplePlugin",
  "type" : "port-mapper",
  "runtimeConfig": {
    "portMappings": [
      {"hostPort": 8080, "containerPort": 80, "protocol": "tcp"}
    ]
  }
}

插件可以定义额外的字段,如果配置文件中传入它不能理解的字段,可能会报错。例外是args字段,可以配置任何值。 

知名args
Arg 说明
labels 传递一系列键值对给插件
JSON
1
2
3
4
"labels" : [
  { "key" : "app", "value" : "myapp" },
  { "key" : "env", "value" : "prod" }
]
ips

用于请求插件分配静态IP地址

JSON
1
"ips": ["10.2.2.42/24", "2001:db8::5"]
知名capabilities
Capability 目的/runtimeConfig
portMappings

传递宿主机端口、容器网络命名空间端口的映射关系

JSON
1
2
3
4
[
  { "hostPort": 8080, "containerPort": 80, "protocol": "tcp" },
  { "hostPort": 8000, "containerPort": 8001, "protocol": "udp" }
]
ipRanges

动态配置分配的IP地址的范围。对于那些负责管理IP地址池(但是不管理单个IP)的运行时,可以传递这些信息给插件

JSON
1
2
3
4
5
[
  [
    { "subnet": "10.1.2.0/24", "rangeStart": "10.1.2.3", "rangeEnd": 10.1.2.99", "gateway": "10.1.2.254" }
  ]
]
bandwidth

用于动态配置网络接口的带宽限制。单位bits/sec

JSON
1
{ "ingressRate": 2048, "ingressBurst": 1600, "egressRate": 4096, "egressBurst": 1600 }  
dns

由运行时动态提供DNS信息

JSON
1
2
3
4
{
  "searches" : [ "internal.yoyodyne.net", "corp.tyrell.net" ]
  "servers": [ "8.8.8.8", "10.0.0.10" ]
}  
ips

由运行时动态的给容器网络接口分配IP地址。如果容器运行时具有IP分配能力,可以传递

JSON
1
[ "10.10.0.1/24", "3ffe:ffff:0:01ff::1/64" ]
mac

容器运行时可以将MAC传递给那些需要mac作为输入的CNI插件

JSON
1
"c2:11:22:33:44:55"  
aliases

提供映射到容器IP地址的别名,便于位于同一个容器网络中的实体可以用此名字访问容器

JSON
1
["my-container", "primary-db"]
配置示例

bridge:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "cniVersion": "0.4.0",
  "name": "dbnet",
  "type": "bridge",
  // 特有字段
  "bridge": "cni0",
  "ipam": {
    "type": "host-local",
    // 特有字段
    "subnet": "10.1.0.0/16",
    "gateway": "10.1.0.1"
  },
  "dns": {
    "nameservers": [ "10.1.0.1" ]
  }
}

macvlan:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
{
  "cniVersion": "0.4.0",
  "name": "wan",
  "type": "macvlan",
  "ipam": {
    "type": "dhcp",
    "routes": [ { "dst": "10.0.0.0/8", "gw": "10.0.0.1" } ]
  },
  "dns": {
    "nameservers": [ "10.0.0.1" ]
  }
}
网络配置列表

使用网络配置列表,可以针对单个容器、按照特定顺序依次调用多个CNI插件,并将前一个插件的结果传递给后一个插件。

注意:网络配置列表的目的并不是为了将容器加入到多个网络,而是为了实现端口映射、流量塑形等额外能力。运行时调用时不会为每个插件指定一个网络接口名称。

可用字段:

字段 说明
cniVersion 字符串。当前配置遵循的CNI规范版本
name 字符串。网络名,在主机范围内必须唯一
disableCheck 布尔。如果为true则运行时不会调用CHECK
plugins

标准的CNI插件的配置列表

运行时调用单个插件时:

  1. 会将name, cniVersion换成列表中的name, cniVersion
  2. 会将上一个插件的结果写入到prevResult字段
  3. 在DEL时,调用插件的顺序和ADD相反
  4. 在相同环境下调用所有插件
  5. 如果出错,则终止后续插件调用
calico配置示例
JSON
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
{
  "name": "k8s-pod-network",
  "cniVersion": "0.3.0",
  "plugins": [
    {
      "type": "calico",
      "log_level": "info",
      "etcd_endpoints": "https://10.0.5.1:2379,https://10.0.2.1:2379,https://10.0.3.1:2379",
      "etcd_key_file": "/etc/cni/net.d/calico-tls/etcd-key",
      "etcd_cert_file": "/etc/cni/net.d/calico-tls/etcd-cert",
      "etcd_ca_cert_file": "/etc/cni/net.d/calico-tls/etcd-ca",
      "mtu": 1440,
      "ipam": {
          "type": "calico-ipam"
      },
      "policy": {
          "type": "k8s"
      },
      "kubernetes": {
          "kubeconfig": "/etc/cni/net.d/calico-kubeconfig"
      }
    },
    {
      "type": "portmap",
      "snat": true,
      "capabilities": {"portMappings": true}
    }
  ]
}
IP地址分配

CNI插件可能需要为网络接口分配IP地址,并为网络接口安装相关的路由规则。为了支持不同的IP管理需求(DHCP、host-local),让IP分配和CNI插件基本功能解耦,规范定义了新的插件类型:IPAM Plugin。调用IPAM Plugin是CNI插件的职责,它应该在适当的时机发起调用,为网络接口获得IP地址。

IPAM插件需要决定:

  1. 网络接口IP/子网
  2. 网关
  3. 路由

并将这些信息返回给“主”CNI插件来应用。IPAM插件的信息来源可能是DHCP协议、本地文件系统中的数据、网络配置ipam段中的配置信息。

IPAM接口

类似于CNI插件,IPAM插件也是通过运行可执行文件来调用的、参数也是通过stdin传递。 IPAM插件必须接收传递给CNI插件的全部环境变量。

如果调用成功,应当以0退出,并跟着如下格式的JSON:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
  "cniVersion": "0.4.0",
  "ips": [
      {
          "version": "<4-or-6>",
          "address": "<ip-and-prefix-in-CIDR>",     // e.g. 192.168.1.3/24
          "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
  }
}

上述返回结果类似于CNI的返回结果,但是不包含interfaces键,IPAM不需要关心网络接口的信息,将IP配置给网络接口是“主”插件的职责。

知名错误码
错误码 说明
1 不兼容的CNI版本
2 网络配置中包含不支持的字段,错误消息中应该指明不支持的键值
3 未知或不存在的容器,隐含运行时不需要进行任何容器网络清理
4 无效的必须环境变量,例如CNI_COMMAND、CNI_CONTAINERID
5 I/O错误,例如无法从stdin读取网络配置
6 无法解析网络配置
7 无效网络配置
11 出现临时性的错误,提示运行时后续重试
环境变量

对于Kubelet来说,CNI的网络配置通常是静态文件,那么针对容器/Pod的运行时参数,都需要通过环境变量传递。

即使使用了插件列表,Kubelet也仅会执行一次CNI调用,这些环境变量对应RuntimeConfig的字段。每个插件看到的都是一样的值。

例如CNI_IFNAME,它总是eth0,是Kubelet给的一个提示,容器的主网络接口的名字是什么。至于CNI插件是否使用eth0,会不会创建eth1,并不受限制。

环境变量 说明
CNI_PATH 从什么目录寻找CNI插件,默认 /opt/cni/bin
CNI_CONTAINERID 容器唯一标识
CNI_NETNS 所在网络命名空间
CNI_IFNAME

网络接口名称,通常是eth0

CNI_COMMAND 调用的CNI接口,例如ADD、DEL、CHECK
CNI_ARGS

CNI参数,格式为 key1=val1;key2=val2

对于Kubelet来说,它肯定会传递以下键:

K8S_POD_NAMESPACE  当前Pod所属命名空间
K8S_POD_NAME  当前Pod的名字
K8S_POD_INFRA_CONTAINER_ID Pod对应的基础容器的ID

NETCONFPATH CNI网络配置所在目录,默认  /etc/cni/net.d
开发CNI

要开发自己的CNI插件,应当先去阅读CNI规范、样例/参考实现。 

cnitool

这是一个小工具,可以执行CNI配置,在已存在的网络命名空间中添加、删除网络接口。

cnitool会搜索 $NETCONFPATH下面所有的 *.conf或 *.json文件,加载它们,然后寻找网络名称和传递给cnitool匹配的网络配置。

安装
Shell
1
2
go get github.com/containernetworking/cni
go install github.com/containernetworking/cni/cnitool

cnitool是参考实现的一部分,因此你可以直接构建参考实现,以获得cnitool: 

Shell
1
2
3
git clone https://github.com/containernetworking/plugins.git
cd plugins
./build_linux.sh
使用

首先需要创建网络命名空间: 

Shell
1
2
3
sudo ip netns add testing
# 自动创建/var/run/netns/testing,也就是/run/netns/testing
# 此文件指向的inode即代表网络命名空间

并创建CNI网络配置:

/etc/cni/net.d/10-myptp.conf
JSON
1
2
3
4
5
6
7
8
9
10
11
{
  "cniVersion": "0.4.0",
  "name": "myptp",
  "type": "ptp",
  "ipMasq": true,
  "ipam": {
    "type": "host-local",
    "subnet": "172.16.29.0/24",
    "routes": [ { "dst": "0.0.0.0/0" } ]
  }
}

然后调用cnitool:

Shell
1
sudo CNI_PATH=./bin cnitool add myptp /var/run/netns/testing

检查配置是否符合预期:

Shell
1
sudo CNI_PATH=./bin cnitool check myptp /var/run/netns/testing

检查命名空间中的网络接口:

Shell
1
2
sudo ip -n testing addr
sudo ip netns exec testing ping -c 172.16.29.1

清理:

Shell
1
2
sudo CNI_PATH=./bin cnitool del myptp /var/run/netns/testing
sudo ip netns del testing
参考实现

CNI项目提供了一个代码库,其中包含了若干参考实现。本文后面的内容会分析其中一些CNI插件的源代码。

运行插件

构建、安装了参考实现,或者你自己编写的CNI插件后,可以利用参考实现项目的scripts/目录中的priv-net-run.sh、docker-run.sh来执行插件,以进行快速的验证。

构建参考实现
Shell
1
2
cd $GOPATH/src/github.com/containernetworking/plugins
./build_linux.sh 
创建网络配置

你需要为被测试的CNI插件创建网络配置:

/etc/cni/net.d/10-mynet.conf
JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
    "cniVersion": "0.2.0",
    "name": "mynet",
    "type": "bridge",
    "bridge": "cni0",
    "isGateway": true,
    "ipMasq": true,
    "ipam": {
        "type": "host-local",
        "subnet": "10.22.0.0/16",
        "routes": [
            { "dst": "0.0.0.0/0" }
        ]
    }
}

/etc/cni/net.d/99-loopback.conf
JSON
1
2
3
4
5
{
    "cniVersion": "0.2.0",
    "name": "lo",
    "type": "loopback"
}
执行CNI插件

调用命令:

Shell
1
2
3
4
5
6
7
8
9
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
 
# lo        Link encap:Local Loopback  
#           inet addr:127.0.0.1  Mask:255.0.0.0

你可以在私有网络命名空间下看到和上面两个CNI配置对应的网络接口。 

运行Docker容器

创建并配置网络命名空间,然后在其中运行Docker容器:

Shell
1
2
3
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
源码分析
libcni
CNI

libcni.CNI,这是CNI提供的一套接口,供容器运行时调用,实现CNI相关的操控。接口规格如下:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type CNI interface {
    // 将容器添加到一组网络,也就是调用一组插件
    AddNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
    // 检查一组网络
    CheckNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error
    // 将容器从一组网络中删除
    DelNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error
    // 获取一组网络的ADD调用结果
    GetNetworkListCachedResult(net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
 
    // 添加、检查、删除、获取单个网络
    AddNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
    CheckNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error
    DelNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error
    GetNetworkCachedResult(net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
 
    // 进行配置校验
    ValidateNetworkList(ctx context.Context, net *NetworkConfigList) ([]string, error)
    ValidateNetwork(ctx context.Context, net *NetworkConfig) ([]string, error)
}
NetworkConfig

NetworkConfigList、NetworkConfig就对应了规范中的网络配置列表、网络配置,JSON部分直接存放在字节数组:

Go
1
2
3
4
5
6
7
8
9
10
11
12
type NetworkConfig struct {
    Network *types.NetConf
    Bytes   []byte
}
 
type NetworkConfigList struct {
    Name         string
    CNIVersion   string
    DisableCheck bool
    Plugins      []*NetworkConfig
    Bytes        []byte
}
NetConf

网络配置,转换为的结构:

Go
1
2
3
4
5
6
7
8
9
10
11
12
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:"-"`
} 
RuntimeConf

RuntimeConf则包含了一次CNI调用中,除了网络配置之外的参数信息。通常由容器运行时根据上下文来构建:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type RuntimeConf struct {
    // 当前针对的容器ID
    ContainerID string
    // 当前操控的网络命名空间
    NetNS       string
    // 当前网络接口名,注意,尽管会通过插件列表调用多个插件,但是这个名字是唯一的,一般是eth0
    IfName      string
    // 参数
    Args        [][2]string
    // 运行时传递给插件的Capability相关的数据
    CapabilityArgs map[string]interface{}
    // 缓存目录
    CacheDir string
}
current.Result

上一个CNI插件的调用结果,存放在此结构中:

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
type current.Result struct {
    CNIVersion string         `json:"cniVersion,omitempty"`
    Interfaces []*Interface   `json:"interfaces,omitempty"`
    IPs        []*IPConfig    `json:"ips,omitempty"`
    Routes     []*types.Route `json:"routes,omitempty"`
    DNS        types.DNS      `json:"dns,omitempty"`
}
 
// 实现了下面的接口
// Result is an interface that provides the result of plugin execution
type types.Result interface {
    // 不需要转换,该结果即可支持的最高CNI版本
    Version() string
 
    // 转换此Result到指定的CNI版本
    GetAsVersion(version string) (Result, error)
 
    // 以JSON格式打印此Result到标准输出
    Print() error
 
    // 以JSON格式打印此Result
    PrintTo(writer io.Writer) error
 
    // 返回JSON格式的Result
    String() string
} 
dockershim调用libcni

dockershim是kubelet中的一个模块,Kubelet通过CRI gRPC调用进程内的dockershim,后者则将CRI请求适配为对Docker守护进程的请求。可以认为dockershim是一个容器运行时。

在创建Pod时,NetworkPlugin.SetUpPod方法会被调用,来为Pod安装网络:

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
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)
    }
 
    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 {
        // 对于Linux,先加入lo网络
        if _, err = plugin.addToNetwork(cniTimeoutCtx, plugin.loNetwork, name, namespace, id,
                        netnsPath, annotations, options); err != nil {
            return err
        }
    }
    // 将Pod加入到默认网络                       *cniNetwork
    _, err = plugin.addToNetwork(cniTimeoutCtx, plugin.getDefaultNetwork(), name, namespace, id,
                        netnsPath, annotations, options)
    return err
}
 
// 表示一个网络
type cniNetwork struct {
    name          string
    // 网络配置列表
    NetworkConfig *libcni.NetworkConfigList
    // 提供CNI的接口
    CNIConfig     libcni.CNI
    Capabilities  []string
}
 
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) {
    // 生成RuntimeConfig
    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
    // 调用CNI接口
    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
}
invoke

此包提供了一些(通过命令行)调用CNI插件的函数。

下面这个函数,根据名字查找CNI插件,并且传递网络配置、来自环境变量的CNI参数,调用插件的ADD命令:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func DelegateAdd(delegatePlugin string, netconf []byte) (types.Result, error) {
    if os.Getenv("CNI_COMMAND") != "ADD" {
        return nil, fmt.Errorf("CNI_COMMAND is not ADD")
    }
 
    paths := filepath.SplitList(os.Getenv("CNI_PATH"))
 
    pluginPath, err := FindInPath(delegatePlugin, paths)
    if err != nil {
        return nil, err
    }
 
    return ExecPluginWithResult(pluginPath, netconf, ArgsFromEnv())
}

ExecPluginWithResult / ExecPluginWithoutResult函数发起CNI调用:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 关注结果
func ExecPluginWithResult(pluginPath string, netconf []byte, args CNIArgs) (types.Result, error) {
    return defaultPluginExec.WithResult(pluginPath, netconf, args)
}
 
// 不关心结果
func ExecPluginWithoutResult(pluginPath string, netconf []byte, args CNIArgs) error {
    return defaultPluginExec.WithoutResult(pluginPath, netconf, args)
}
// 示例
return invoke.ExecPluginWithoutResult(pluginPath, netconfBytes, &invoke.Args{
    Command:       "DEL",
    ContainerID:   args.ContainerID,
    NetNS:         args.Netns,
    PluginArgsStr: args.Args,
    IfName:        ifName,
    Path:          args.Path,
})

 

unitest 

参考实现的测试用例基于Ginkgo,本节阅读loopback插件的测试用例,依此了解如何调用、测试CNI插件。

测试套件
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
package main_test
 
import (
    "github.com/onsi/gomega/gexec"
 
    . "github.com/onsi/ginkgo"
    . "github.com/onsi/gomega"
 
    "testing"
)
 
var pathToLoPlugin string
 
func TestLoopback(t *testing.T) {
    RegisterFailHandler(Fail)
    // 入口点,启动测试套件
    RunSpecs(t, "plugins/main/loopback")
}
 
var _ = BeforeSuite(func() {
    var err error
    // 使用go build来执行构建,返回临时目录中的二进制文件的路径
    pathToLoPlugin, err = gexec.Build("github.com/containernetworking/plugins/plugins/main/loopback")
    Expect(err).NotTo(HaveOccurred())
})
 
var _ = AfterSuite(func() {
    // 清理套件启动后构建的二进制文件
    gexec.CleanupBuildArtifacts()
})
测试用例
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
package main_test
 
import (
    "fmt"
    "net"
    "os/exec"
    "strings"
 
    "github.com/containernetworking/plugins/pkg/ns"
    "github.com/containernetworking/plugins/pkg/testutils"
    . "github.com/onsi/ginkgo"
    . "github.com/onsi/gomega"
    "github.com/onsi/gomega/gbytes"
    "github.com/onsi/gomega/gexec"
)
 
// 一组用例
var _ = Describe("Loopback", func() {
    var (
        networkNS ns.NetNS
        command   *exec.Cmd
        environ   []string
    )
    // 准备工作
    BeforeEach(func() {
        // 插件命令
        command = exec.Command(pathToLoPlugin)
        var err error
        // 通过unshare系统调用创建新NS,但是不会切换到此NS
        networkNS, err = testutils.NewNS()
        Expect(err).NotTo(HaveOccurred())
 
        // 准备调用CNI插件需要的环境变量
        environ = []string{
            fmt.Sprintf("CNI_CONTAINERID=%s", "dummy"),
            // CNI插件需要在哪个命名空间中运作
            fmt.Sprintf("CNI_NETNS=%s", networkNS.Path()),
            fmt.Sprintf("CNI_IFNAME=%s", "this is ignored"),
            fmt.Sprintf("CNI_ARGS=%s", "none"),
            fmt.Sprintf("CNI_PATH=%s", "/some/test/path"),
        }
        // 准备插件输入参数
        command.Stdin = strings.NewReader(`{ "name": "loopback-test", "cniVersion": "0.1.0" }`)
    })
 
    // 清理工作,关闭命名空间描述符、卸载命名空间
    AfterEach(func() {
        Expect(networkNS.Close()).To(Succeed())
        Expect(testutils.UnmountNS(networkNS)).To(Succeed())
    })
 
    Context("when given a network namespace", func() {
        It("sets the lo device to UP", func() {
            // 调用ADD接口
            command.Env = append(environ, fmt.Sprintf("CNI_COMMAND=%s", "ADD"))
 
            session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
            Expect(err).NotTo(HaveOccurred())
 
            // 对命令的标准输出进行断言
            Eventually(session).Should(gbytes.Say(`{.*}`))
            Eventually(session).Should(gexec.Exit(0))
 
            var lo *net.Interface
            
            err = networkNS.Do(func(ns.NetNS) error {
                // 如果CNI插件运行成功,则在此NS中会出现一个lo网卡
                var err error
                lo, err = net.InterfaceByName("lo")
                return err
            })
            Expect(err).NotTo(HaveOccurred())
 
            Expect(lo.Flags & net.FlagUp).To(Equal(net.FlagUp))
        })
 
        It("sets the lo device to DOWN", func() {
            // 调用DEL接口
            command.Env = append(environ, fmt.Sprintf("CNI_COMMAND=%s", "DEL"))
 
            session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
            Expect(err).NotTo(HaveOccurred())
 
            Eventually(session).Should(gbytes.Say(``))
            Eventually(session).Should(gexec.Exit(0))
 
            var lo *net.Interface
            err = networkNS.Do(func(ns.NetNS) error {
                // 如果CNI插件运行成功,则在此NS中不存在lo网卡
                var err error
                lo, err = net.InterfaceByName("lo")
                return err
            })
            Expect(err).NotTo(HaveOccurred())
 
            Expect(lo.Flags & net.FlagUp).NotTo(Equal(net.FlagUp))
        })
    })
})
skel

这个包提供了CNI插件开发的骨架代码,它实现了参数解析和校验,你可以将其作为库使用,简化CNI开发。

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
package skel
 
import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "os"
    "strings"
 
    "github.com/containernetworking/cni/pkg/types"
    "github.com/containernetworking/cni/pkg/version"
)
 
// 来自环境变量、标准输入的CNI参数
type CmdArgs struct {
    ContainerID string
    Netns       string
    IfName      string
    Args        string
    Path        string
    StdinData   []byte
}
 
// 解析并得到CmdArgs、执行CNI调用
type dispatcher struct {
    Getenv func(string) string
    Stdin  io.Reader
    Stdout io.Writer
    Stderr io.Writer
 
    ConfVersionDecoder version.ConfigDecoder
    VersionReconciler  version.Reconciler
}
 
type reqForCmdEntry map[string]bool
 
// internal only error to indicate lack of required environment variables
type missingEnvError struct {
    msg string
}
 
func (e missingEnvError) Error() string {
    return e.msg
}
 
// 从环境变量解析参数
func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, 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", // 容器ID
            &contID,
            reqForCmdEntry{
                "ADD":   true,
                "CHECK": true,
                "DEL":   true,
            },
        },
        {
            "CNI_NETNS", // 网络NS文件路径
            &netns,
            reqForCmdEntry{
                "ADD":   true,
                "CHECK": true,
                "DEL":   false,
            },
        },
        {
            "CNI_IFNAME", // 网络接口名
            &ifName,
            reqForCmdEntry{
                "ADD":   true,
                "CHECK": true,
                "DEL":   true,
            },
        },
        {
            "CNI_ARGS", // CNI参数
            &args,
            reqForCmdEntry{
                "ADD":   false,
                "CHECK": false,
                "DEL":   false,
            },
        },
        {
            "CNI_PATH", // CNI二进制文件路径
            &path,
            reqForCmdEntry{
                "ADD":   true,
                "CHECK": true,
                "DEL":   true,
            },
        },
    }
 
    argsMissing := make([]string, 0)
    for _, v := range vars {
        *v.val = t.Getenv(v.name)
        if *v.val == "" {
            if v.reqForCmd[cmd] || v.name == "CNI_COMMAND" {
                argsMissing = append(argsMissing, v.name)
            }
        }
    }
 
    if len(argsMissing) > 0 {
        joined := strings.Join(argsMissing, ",")
        return "", nil, missingEnvError{fmt.Sprintf("required env variables [%s] missing", joined)}
    }
 
    if cmd == "VERSION" {
        t.Stdin = bytes.NewReader(nil)
    }
 
    stdinData, err := ioutil.ReadAll(t.Stdin)
    if err != nil {
        return "", nil, fmt.Errorf("error reading from stdin: %v", err)
    }
 
    cmdArgs := &CmdArgs{
        ContainerID: contID,
        Netns:       netns,
        IfName:      ifName,
        Args:        args,
        Path:        path,
        StdinData:   stdinData,
    }
    return cmd, cmdArgs, nil
}
 
func createTypedError(f string, args ...interface{}) *types.Error {
    return &types.Error{
        Code: 100,
        Msg:  fmt.Sprintf(f, args...),
    }
}
 
// 检查CNI并执行调用
func (t *dispatcher) checkVersionAndCall(cmdArgs *CmdArgs, pluginVersionInfo
        version.PluginInfo, toCall func(*CmdArgs) error) error {
    configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData)
    if err != nil {
        return err
    }
    verErr := t.VersionReconciler.Check(configVersion, pluginVersionInfo)
    if verErr != nil {
        return &types.Error{
            Code:    types.ErrIncompatibleCNIVersion,
            Msg:     "incompatible CNI versions",
            Details: verErr.Details(),
        }
    }
 
    return toCall(cmdArgs)
}
 
// 校验配置
func validateConfig(jsonBytes []byte) error {
    var conf struct {
        Name string `json:"name"`
    }
    if err := json.Unmarshal(jsonBytes, &conf); err != nil {
        return fmt.Errorf("error reading network config: %s", err)
    }
    if conf.Name == "" {
        return fmt.Errorf("missing network name")
    }
    return nil
}
 
// CNI调用的核心逻辑
func (t *dispatcher) pluginMain(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error,
        versionInfo version.PluginInfo, about string) *types.Error {
    // 解析命令和参数
    cmd, cmdArgs, err := t.getCmdArgsFromEnv()
    if err != nil {
        // 如果没有指定命令,则打印About后退出
        if _, ok := err.(missingEnvError); ok && t.Getenv("CNI_COMMAND") == "" && about != "" {
            fmt.Fprintln(t.Stderr, about)
            return nil
        }
        return createTypedError(err.Error())
    }
    // 进行配置(标准输入)校验
    if cmd != "VERSION" {
        err = validateConfig(cmdArgs.StdinData)
        if err != nil {
            return createTypedError(err.Error())
        }
    }
 
    switch cmd {
    case "ADD":
        // ADD接口,直接转调函数
        err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdAdd)
    case "CHECK":
        // CHECK接口
        // 检查版本
        configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData)
        if err != nil {
            return createTypedError(err.Error())
        }
        if gtet, err := version.GreaterThanOrEqualTo(configVersion, "0.4.0"); err != nil {
            return createTypedError(err.Error())
        } else if !gtet {
            return &types.Error{
                Code: types.ErrIncompatibleCNIVersion,
                Msg:  "config version does not allow CHECK",
            }
        }
        for _, pluginVersion := range versionInfo.SupportedVersions() {
            gtet, err := version.GreaterThanOrEqualTo(pluginVersion, configVersion)
            if err != nil {
                return createTypedError(err.Error())
            } else if gtet {
                // 转调函数
                if err := t.checkVersionAndCall(cmdArgs, versionInfo, cmdCheck); err != nil {
                    return createTypedError(err.Error())
                }
                return nil
            }
        }
        return &types.Error{
            Code: types.ErrIncompatibleCNIVersion,
            Msg:  "plugin version does not allow CHECK",
        }
    case "DEL":
        // DEL接口,直接转调函数
        err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdDel)
    case "VERSION":
        // 打印版本信息
        err = versionInfo.Encode(t.Stdout)
    default:
        return createTypedError("unknown CNI_COMMAND: %v", cmd)
    }
 
    if err != nil {
        if e, ok := err.(*types.Error); ok {
            // don't wrap Error in Error
            return e
        }
        return createTypedError(err.Error())
    }
    return nil
}
 
// PluginMainWithError是一个CNI的核心main逻辑骨架,调用后产生的错误需要你自行处理
// 实际上就是创建dispatcher,并调用它的pluginMain方法
func PluginMainWithError(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error,
        versionInfo version.PluginInfo, about string) *types.Error {
    return (&dispatcher{
        Getenv: os.Getenv,
        Stdin:  os.Stdin,
        Stdout: os.Stdout,
        Stderr: os.Stderr,
    }).pluginMain(cmdAdd, cmdCheck, cmdDel, versionInfo, about)
}
 
// 此函数是一个CNI的核心main逻辑骨架,支持自动化的错误处理
//
// 调用者(CNI插件)需要指明它支持哪些CNI规范版本
//
// 如果没有指定CNI_COMMAND,则调用者提供的about打印到stderr
// about的推荐格式: CNI plugin <foo> v<version>
//
// cmdAdd, cmdCheck或cmdDel调用出错,则PluginMain打印错误JSON到stdout,并os.Exit(1)
//
// 如果需要定制错误处理,调用PluginMainWithError()
func PluginMain(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error, versionInfo
        version.PluginInfo, about string) {
    if e := PluginMainWithError(cmdAdd, cmdCheck, cmdDel, versionInfo, about); e != nil {
        if err := e.Print(); err != nil {
            log.Print("Error writing error JSON to stdout: ", err)
        }
        os.Exit(1)
    }
}

有了skel,我们实现自己的CNI插件时,只需要编写几个函数就可以了。 

loopback 

这个插件很简单,就是在命名空间中添加一个lo接口:

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
package main
 
import (
    "encoding/json"
    "errors"
    "fmt"
    "net"
 
    "github.com/vishvananda/netlink"
 
    "github.com/containernetworking/cni/pkg/skel"
    "github.com/containernetworking/cni/pkg/types"
    "github.com/containernetworking/cni/pkg/types/current"
    "github.com/containernetworking/cni/pkg/version"
 
    "github.com/containernetworking/plugins/pkg/ns"
    bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
)
// 从标准输入的JSON解析CNI配置
func parseNetConf(bytes []byte) (*types.NetConf, error) {
    conf := &types.NetConf{}
    if err := json.Unmarshal(bytes, conf); err != nil {
        return nil, fmt.Errorf("failed to parse network config: %v", err)
    }
    // 可能存在前序被调用插件
    if conf.RawPrevResult != nil {
        // 前序调用结果是存放在网络配置中的
        if err := version.ParsePrevResult(conf); err != nil {
            return nil, fmt.Errorf("failed to parse prevResult: %v", err)
        }
        // 尝试解析前序结果,失败则出错
        if _, err := current.NewResultFromResult(conf.PrevResult); err != nil {
            return nil, fmt.Errorf("failed to convert result to current version: %v", err)
        }
    }
 
    return conf, nil
}
 
func cmdAdd(args *skel.CmdArgs) error {
    // 解析出配置
    conf, err := parseNetConf(args.StdinData)
    if err != nil {
        return err
    }
 
    var v4Addr, v6Addr *net.IPNet
    // 强制使用lo作为网络接口名称,忽略配置
    args.IfName = "lo"
 
    // 在路径args.Netns代表的网络命名空间下执行
    err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
        // 获取Link对象,lo接口不需要添加,默认就存在,只是可能没有up
        link, err := netlink.LinkByName(args.IfName)
        if err != nil {
            return err // not tested
        }
        // 设置网络接口为UP状态
        err = netlink.LinkSetUp(link)
        if err != nil {
            return err // not tested
        }
 
        // 获得网络接口的IPv4地址接口
        v4Addrs, err := netlink.AddrList(link, netlink.FAMILY_V4)
        if err != nil {
            return err // not tested
        }
        // 如果有IPv4的地址,则确认是loopback地址
        if len(v4Addrs) != 0 {
            v4Addr = v4Addrs[0].IPNet
            // sanity check that this is a loopback address
            for _, addr := range v4Addrs {
                if !addr.IP.IsLoopback() {
                    return fmt.Errorf("loopback interface found with non-loopback address %q", addr.IP)
                }
            }
        }
        // 获得网络接口的IPv6地址接口
        // 如果有IPv6的地址,则确认是loopback地址
        v6Addrs, err := netlink.AddrList(link, netlink.FAMILY_V6)
        if err != nil {
            return err // not tested
        }
        if len(v6Addrs) != 0 {
            v6Addr = v6Addrs[0].IPNet
            // sanity check that this is a loopback address
            for _, addr := range v6Addrs {
                if !addr.IP.IsLoopback() {
                    return fmt.Errorf("loopback interface found with non-loopback address %q", addr.IP)
                }
            }
        }
 
        return nil
    })
    if err != nil {
        return err // not tested
    }
 
    var result types.Result
    // 如果有来自上一个CNI插件的结果,则透明的传递下去
    if conf.PrevResult != nil {
        // If loopback has previous result which passes from previous CNI plugin,
        // loopback should pass it transparently
        result = conf.PrevResult
    } else {
        // 否则,构建一个结果,作为PrevResult
        loopbackInterface := &current.Interface{
            Name: args.IfName,
            // lo不关心L2地址
            Mac: "00:00:00:00:00:00",
            Sandbox: args.Netns,
        }
        // 将本次添加的IP地址纳入结果
        r := &current.Result{
            CNIVersion: conf.CNIVersion,
            Interfaces: []*current.Interface{ loopbackInterface },
        }
 
        if v4Addr != nil {
            r.IPs = append(r.IPs, &current.IPConfig{
                Version:   "4",
                Interface: current.Int(0),
                Address:   *v4Addr,
            })
        }
 
        if v6Addr != nil {
            r.IPs = append(r.IPs, &current.IPConfig{
                Version:   "6",
                Interface: current.Int(0),
                Address:   *v6Addr,
            })
        }
 
        result = r
    }
 
    return types.PrintResult(result, conf.CNIVersion)
}
 
// 只是把lo关闭,不能删除
func cmdDel(args *skel.CmdArgs) error {
    if args.Netns == "" {
        return nil
    }
    args.IfName = "lo" // ignore config, this only works for loopback
    err := ns.WithNetNSPath(args.Netns, func(ns.NetNS) error {
        link, err := netlink.LinkByName(args.IfName)
        if err != nil {
            return err // not tested
        }
 
        err = netlink.LinkSetDown(link)
        if err != nil {
            return err // not tested
        }
 
        return nil
    })
    if err != nil {
        return err // not tested
    }
 
    return nil
}
 
// 检查lo接口是否启动
func cmdCheck(args *skel.CmdArgs) error {
    args.IfName = "lo" // ignore config, this only works for loopback
 
    return ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
        link, err := netlink.LinkByName(args.IfName)
        if err != nil {
            return err
        }
        // link属性标记,位域
        if link.Attrs().Flags&net.FlagUp != net.FlagUp {
            return errors.New("loopback interface is down")
        }
 
        return nil
    })
}
 
 
 
func main() {
    // 调用skel
    skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("loopback"))
}
bridge
简介 

使用该插件,同一主机上的所有容器被连接到一个网桥上,网桥位于宿主机的网络命名空间中。一个veth对,一端接着网桥,另一端接着容器。IP地址仅仅在veth对连接容器的那一端分配。 

网桥自身也可以分配IP地址,这样它可以作为所有容器的网关。如果不分配,那么网桥工作在纯L2模式,如果容器有对外访问需求(也就是不仅仅需要和本机其它容器通信),则网桥需要桥接到宿主机的某个网络接口。

使用此插件时,需要指定网桥的名字。示例配置:

JSON
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
{
    "cniVersion": "0.3.1",
    "name": "mynet",
    "type": "bridge",
    // 使用或创建的网桥的名字,默认cni0
    "bridge": "mynet0",
    // 如果为true则为网桥分配IP地址,默认false
    "isGateway": true,
    // isGateway为true的前提下,如果设置为true,则设置网桥IP为命名空间的默认路由
    "isDefaultGateway": true,
    // 是否需要为网桥设置新的IP地址(如果地址变化),默认false
    "forceAddress": false,
    // 在宿主机上,为来自此容器,目标是主机之外的流量设置IP遮掩(SNAT),默认值由内核选择
    "ipMasq": true,
    // 为网桥上的接口设置hairpin模式,默认false
    // 默认情况下,网桥不会将从端口A发来的封包,再发回A去,这会导致容器无法访问自己
    "hairpinMode": true,
    // 使用的IPAM插件
    "ipam": {
        "type": "host-local",
        "subnet": "10.10.0.0/16"
    }
    // 将网桥设置为混杂模式,默认false
    // 混杂模式下,网络接口监听任何封包,不管它的目的地(MAC)是不是自己
    promiscMode: false
    // 分配VLAN tag,默认不分配
    // 如果指定,则在veth对的宿主机端设置VLAN tag,并且启用网桥的vlan_filtering特性
    vlan: 0
}

纯L2模式配置:

JSON
1
2
3
4
5
6
7
{
    "cniVersion": "0.3.1",
    "name": "mynet",
    "type": "bridge",
    "bridge": "mynet0",
    "ipam": {}
}
核心代码
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
package main
 
import (
    "encoding/json"
    "errors"
    "fmt"
    "io/ioutil"
    "net"
    "runtime"
    "syscall"
    "time"
 
    "github.com/j-keck/arping"
    "github.com/vishvananda/netlink"
 
    "github.com/containernetworking/cni/pkg/skel"
    "github.com/containernetworking/cni/pkg/types"
    "github.com/containernetworking/cni/pkg/types/current"
    "github.com/containernetworking/cni/pkg/version"
    "github.com/containernetworking/plugins/pkg/ip"
    "github.com/containernetworking/plugins/pkg/ipam"
    "github.com/containernetworking/plugins/pkg/ns"
    "github.com/containernetworking/plugins/pkg/utils"
    bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
    "github.com/containernetworking/plugins/pkg/utils/sysctl"
)
 
// For testcases to force an error after IPAM has been performed
var debugPostIPAMError error
 
const defaultBrName = "cni0"
 
type NetConf struct {
    types.NetConf
    // 在标准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"`
}
 
 
// 网关信息
type gwInfo struct {
    // IP地址和子网掩码
    gws               []net.IPNet
    // IPv4还是IPv6
    family            int
    // 在网关列表中,到此网关为止,是否发现默认路由
    defaultRouteFound bool
}
 
func init() {
    // 确保main函数在主线程(线程组Leader)上运行
    // 命名空间操作(unshare、setns)为单个线程设计,我们需要确保main函数
    // 不会跳转到其它OS线程上
    runtime.LockOSThread()
}
 
func loadNetConf(bytes []byte) (*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)
    }
    if n.Vlan < 0 || n.Vlan > 4094 {
        return nil, "", fmt.Errorf("invalid VLAN ID %d (must be between 0 and 4094)", n.Vlan)
    }
    return n, n.CNIVersion, nil
}
 
// 处理IPAM插件的结果,为每个地址族:
// 1. 计算、编译网关地址列表
// 2. 如果需要,添加一个默认路由
func calcGateways(result *current.Result, n *NetConf) (*gwInfo, *gwInfo, error) {
 
    gwsV4 := &gwInfo{}
    gwsV6 := &gwInfo{}
 
    // 便利IPAM返回的每一个IPConfig
    for _, ipc := range result.IPs {
 
        // Determine if this config is IPv4 or IPv6
        var gws *gwInfo
        // 代表一个IP网络信息(网络号+掩码)
        defaultNet := &net.IPNet{}
        switch {
        case ipc.Address.IP.To4() != nil:
            gws = gwsV4
            // 地址族信息
            gws.family = netlink.FAMILY_V4
            defaultNet.IP = net.IPv4zero
        case len(ipc.Address.IP) == net.IPv6len:
            gws = gwsV6
            gws.family = netlink.FAMILY_V6
            defaultNet.IP = net.IPv6zero
        default:
            return nil, nil, fmt.Errorf("Unknown IP object: %v", ipc)
        }
        // 计算掩码
        defaultNet.Mask = net.IPMask(defaultNet.IP)
 
        // 这里仅仅是为了获得*int,以便后续设置网络接口索引
        ipc.Interface = current.Int(2)
 
        // 如果IPAM没有提供网关,则计算出网关地址
        if ipc.Gateway == nil && n.IsGW {
            // 使用网络的第1个地址作为网关
            ipc.Gateway = calcGatewayIP(&ipc.Address)
        }
 
        // 如有必要(此网桥作为默认路由),为当前地址族添加默认路由(使用当前网关地址)
        if n.IsDefaultGW && !gws.defaultRouteFound {
            for _, route := range result.Routes {
                if route.GW != nil && defaultNet.String() == route.Dst.String() {
                    gws.defaultRouteFound = true
                    break
                }
            }
            if !gws.defaultRouteFound {
                // 添加一条默认路由,可能使用计算出的网关地址
                result.Routes = append(
                    result.Routes,
                    &types.Route{Dst: *defaultNet, GW: ipc.Gateway},
                )
                gws.defaultRouteFound = true
            }
        }
 
        // 如果当前网络作为网关,则加入到网关列表
        if n.IsGW {
            gw := net.IPNet{
                IP:   ipc.Gateway,
                Mask: ipc.Address.Mask,
            }
            gws.gws = append(gws.gws, gw)
        }
    }
    return gwsV4, gwsV6, nil
}
 
// 确保某个(IPAM分配的)IP地址被设置到网桥上
func ensureAddr(br netlink.Link, family int, ipn *net.IPNet, forceAddress bool) error {
    // 获得网络接口现有地址列表
    addrs, err := netlink.AddrList(br, family)
    if err != nil && err != syscall.ENOENT {
        return fmt.Errorf("could not get list of IP addresses: %v", err)
    }
 
    // 需要被设置的IP地址
    ipnStr := ipn.String()
    for _, a := range addrs {
 
        // 已经被设置了,不做操作
        if a.IPNet.String() == ipnStr {
            return nil
        }
 
        // 如果对应的子网不重叠,多个IPv6地址可以存在于网桥上
        // 对于IPv4,或者子网重叠的Ipv6,如果forceAddress为true,重新配置IP地址
        if family == netlink.FAMILY_V4 || a.IPNet.Contains(ipn.IP) || ipn.Contains(a.IPNet.IP) {
            if forceAddress {
                // 删除现有IP地址
                if err = deleteAddr(br, a.IPNet); err != nil {
                    return err
                }
            } else {
                return fmt.Errorf("%q already has an IP address different from %v", br.Attrs().Name, ipnStr)
            }
        }
    }
 
    // 为网桥添加新IP地址
    addr := &netlink.Addr{IPNet: ipn, Label: ""}
    if err := netlink.AddrAdd(br, addr); err != nil && err != syscall.EEXIST {
        return fmt.Errorf("could not add IP address to %q: %v", br.Attrs().Name, err)
    }
 
    // 将网桥的MAC地址设置给自己
    // otherwise, the bridge will take the
    // lowest-numbered mac on the bridge, and will change as ifs churn
    if err := netlink.LinkSetHardwareAddr(br, br.Attrs().HardwareAddr); err != nil {
        return fmt.Errorf("could not set bridge's mac: %v", err)
    }
 
    return nil
}
 
// 将IP地址从网络接口上删除
func deleteAddr(br netlink.Link, ipn *net.IPNet) error {
    addr := &netlink.Addr{IPNet: ipn, Label: ""}
 
    if err := netlink.AddrDel(br, addr); err != nil {
        return fmt.Errorf("could not remove IP address from %q: %v", br.Attrs().Name, err)
    }
 
    return nil
}
 
// 根据名称获取网桥
func bridgeByName(name string) (*netlink.Bridge, error) {
    l, err := netlink.LinkByName(name)
    if err != nil {
        return nil, fmt.Errorf("could not lookup %q: %v", name, err)
    }
    // 通过CAST判断网络接口是不是网桥
    br, ok := l.(*netlink.Bridge)
    if !ok {
        return nil, fmt.Errorf("%q already exists but is not a bridge", name)
    }
    return br, nil
}
 
// 确保网桥存在、并且已经配置好
func ensureBridge(brName string, mtu int, promiscMode, vlanFiltering bool) (*netlink.Bridge, error) {
    // 网桥对象
    br := &netlink.Bridge{
        LinkAttrs: netlink.LinkAttrs{
            Name: brName,
            MTU:  mtu,
            // 让内核使用默认的发送队列长度(txqueuelen); leaving it unset
            // 如果不设置 ,则为0,也就是使用0长的发送队列长度,这会导致FIFO流量塑形器(traffic shapers)
            // 不能正常工作,因为它使用发送队列长度作为默认的封包限制
            TxQLen: -1,
        },
    }
    if vlanFiltering {
        // 启用VLAN过滤,使用一个网桥即可管理所有VLAN,而不是为每个VLAN创建独立网桥,3.8开始支持
        br.VlanFiltering = &vlanFiltering
    }
 
    // 添加网桥
    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)
        }
    }
 
    // 重新读取网桥配置
    br, err = bridgeByName(brName)
    if err != nil {
        return nil, err
    }
 
    // 不接受IPv6路由通告,自己管理关于此接口的路由
    _, _ = sysctl.Sysctl(fmt.Sprintf("net/ipv6/conf/%s/accept_ra", brName), "0")
 
    // 启动网桥
    if err := netlink.LinkSetUp(br); err != nil {
        return nil, err
    }
 
    return br, nil
}
 
// 确保VLAN接口就绪
func ensureVlanInterface(br *netlink.Bridge, vlanId int) (netlink.Link, error) {
    name := fmt.Sprintf("%s.%d", br.Name, vlanId)
    // 检查VLAN接口是否已经存在
    brGatewayVeth, err := netlink.LinkByName(name)
    if err != nil {
        if err.Error() != "Link not found" { // 根据字符串判断靠谱么?
            return nil, fmt.Errorf("failed to find interface %q: %v", name, err)
        }
        hostNS, err := ns.GetCurrentNS()
        if err != nil {
            return nil, fmt.Errorf("faild to find host namespace: %v", err)
        }
 
        // 使用veth将网桥和VLAN接口连接起来(和容器网络接口一样)
        // 创建veth对,一端连在网桥,另外一端连接在hostNS的VLAN网络接口
        _, brGatewayIface, err := setupVeth(hostNS, br, name, br.MTU, false, vlanId)
        if err != nil {
            return nil, fmt.Errorf("faild to create vlan gateway %q: %v", name, err)
        }
        // 重新查找,确保OK
        brGatewayVeth, err = netlink.LinkByName(brGatewayIface.Name)
        if err != nil {
            return nil, fmt.Errorf("failed to lookup %q: %v", brGatewayIface.Name, err)
        }
    }
 
    return brGatewayVeth, nil
}
 
// 创建veth对
func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int,
        hairpinMode bool, vlanID int) (*current.Interface, *current.Interface, error) {
    contIface := &current.Interface{}
    hostIface := &current.Interface{}
    //     容器网络命名空间
    err := netns.Do(func(hostNS ns.NetNS) error {
        // 创建veth对,在容器网络命名空间中调用,会自动将对端移动到hostNS
        hostVeth, containerVeth, err := ip.SetupVeth(ifName, mtu, hostNS)
        if err != nil {
            return err
        }
        // 从veth收集容器端网络接口的名字、MAC地址
        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
    }
 
    // 由于移动到了根网络命名空间,hostVeth的index已经变了,需要重新获取网络接口对象
    hostVeth, err := netlink.LinkByName(hostIface.Name)
    if err != nil {
        return nil, nil, fmt.Errorf("failed to lookup %q: %v", hostIface.Name, err)
    }
    // 设置宿主端的veth的L2地址。如果这个地址不正确,则无法进行L2通信,这个veth是容器和外部的桥梁
    hostIface.Mac = hostVeth.Attrs().HardwareAddr.String()
 
    // 将hostVeth对接到网桥上
    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)
    }
 
    // 为宿主端的veth设置Hairpin模式
    if err = netlink.LinkSetHairpin(hostVeth, hairpinMode); err != nil {
        return nil, nil, fmt.Errorf("failed to setup hairpin mode for %v: %v", hostVeth.Attrs().Name, err)
    }
 
    // 如果指定了VLAN,则在网桥上(实际上是veth的宿主机端,桥的一个端口)添加VLAN filter entry
    if vlanID != 0 {
        // bridge vlan add dev DEV vid VID [ pvid ] [ untagged ] [ self ] [ master ]
        err = netlink.BridgeVlanAdd(hostVeth, uint16(vlanID), true, true, false, true)
        if err != nil {
            return nil, nil, fmt.Errorf("failed to setup vlan tag on interface %q: %v", hostIface.Name, err)
        }
    }
 
    return hostIface, contIface, nil
}
 
// 网关IP,简单的+1
func calcGatewayIP(ipn *net.IPNet) net.IP {
    nid := ipn.IP.Mask(ipn.Mask)
    return ip.NextIP(nid)
}
 
// 创建网桥
func setupBridge(n *NetConf) (*netlink.Bridge, *current.Interface, error) {
    vlanFiltering := false
    if n.Vlan != 0 {
        vlanFiltering = true
    }
    // 如果需要,创建并配置网桥
    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
}
 
// 禁用IP6的地址冲突检测(Duplicate Address Detection,DAD)
// 禁用的原因是Hairpin模式会导致网络接口看到自己的DAD封包
func disableIPV6DAD(ifName string) error {
    // ehanced_dad sends a nonce with the DAD packets, so that we can safely
    // ignore ourselves
    enh, err := ioutil.ReadFile(fmt.Sprintf("/proc/sys/net/ipv6/conf/%s/enhanced_dad", ifName))
    if err == nil && string(enh) == "1\n" {
        return nil
    }
    f := fmt.Sprintf("/proc/sys/net/ipv6/conf/%s/accept_dad", ifName)
    return ioutil.WriteFile(f, []byte("0"), 0644)
}
 
// 启用IP转发
func enableIPForward(family int) error {
    if family == netlink.FAMILY_V4 {
        // 启用IPv4转发
        return ip.EnableIP4Forward()
    }
    // 启用IPv6转发
    return ip.EnableIP6Forward()
}
 
func cmdAdd(args *skel.CmdArgs) error {
    var success bool = false
 
    // 解析配置
    n, cniVersion, err := loadNetConf(args.StdinData)
    if err != nil {
        return err
    }
 
    if n.IsDefaultGW {
        n.IsGW = true
    }
    // 不支持同时设置Hairpin和Promisc模式
    if n.HairpinMode && n.PromiscMode {
        return fmt.Errorf("cannot set hairpin mode and promiscous mode at the same time.")
    }
 
    // 如有必要,创建网桥,并进行配置
    br, brInterface, err := setupBridge(n)
    if err != nil {
        return err
    }
 
    // 当前处理的容器网络命名空间
    netns, err := ns.GetNS(args.Netns)
    if err != nil {
        return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
    }
    defer netns.Close()
 
    // 为容器网络命名空间创建VETH对
    // 创建VETH对,一端插入容器,成为containerInterface
    // 另外一端插到网桥上,即hostInterface
    hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName,
            n.MTU, n.HairpinMode, n.Vlan)
    if err != nil {
        return err
    }
 
    // 返回接口列表:网桥、宿主机端veth、容器命名空间内eth
    result := &current.Result{
        CNIVersion: cniVersion,
        Interfaces: []*current.Interface{
            brInterface, hostInterface, containerInterface,
        },
    }
 
    isLayer3 := n.IPAM.Type != ""
 
    // 如果是L3模式,向IPAM申请IP地址
    if isLayer3 {
        // 调用IPAM插件,传递自己的配置(标准输入)给它
        r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
        if err != nil {
            return err
        }
 
        // 如果调用失败,释放掉可能已经分配的IP地址
        defer func() {
            if !success {
                ipam.ExecDel(n.IPAM.Type, args.StdinData)
            }
        }()
 
        // 转换Result为当前CNI版本
        ipamResult, err := current.NewResultFromResult(r)
        if err != nil {
            return err
        }
        // 获取IPAM分配的地址、路由
        result.IPs = ipamResult.IPs
        result.Routes = ipamResult.Routes
 
        if len(result.IPs) == 0 {
            return errors.New("IPAM plugin returned missing IP config")
        }
 
        // 为每个地址族收集路由信息
        gwsV4, gwsV6, err := calcGateways(result, n)
        if err != nil {
            return err
        }
 
        // 在容器网络命名空间配置IP地址和路由
        if err := netns.Do(func(_ ns.NetNS) error {
            // 为了应对网桥开启hairpin的情况,禁用DAD
            // Hairpin会导致回响(echo)neighbor solicitation packets, 进而导致DAD错误
            for _, ipc := range result.IPs {
                if ipc.Version == "6" && (n.HairpinMode || n.PromiscMode) {
                    // 禁用IPv6 Duplicate Address Detection
                    if err := disableIPV6DAD(args.IfName); err != nil {
                        return err
                    }
                    break
                }
            }
 
            // 使用IPAM的调用结果来分配IP地址给容器接口,并且添加路由
            if err := ipam.ConfigureIface(args.IfName, result); err != nil {
                return err
            }
            return nil
        }); err != nil {
            return err
        }
 
        // 检查网桥Port状态
        retries := []int{0, 50, 500, 1000, 1000}
        for idx, sleep := range retries {
            time.Sleep(time.Duration(sleep) * time.Millisecond)
            // 宿主机端(连接在网桥上)的VETH端
            hostVeth, err := netlink.LinkByName(hostInterface.Name)
            if err != nil {
                return err
            }
            // 应当处于Up状态(可以发封包了)
            if hostVeth.Attrs().OperState == netlink.OperUp {
                break
            }
 
            if idx == len(retries)-1 {
                return fmt.Errorf("bridge port in error state: %s", hostVeth.Attrs().OperState)
            }
        }
 
        // 发送无故ARP(也叫免费ARP)请求(gratuitous arp) —— 请求容器接口自身IP地址对应
        // 的MAC,价值是:
        //   1. 验证IP是否冲突,如果此ARP收到应答,则提示网络中有人用了和自己相同的IP
        //   2. 如果当前接口改变了MAC地址,可以通知网络中其它主机及时更新ARP缓存
        if err := netns.Do(func(_ ns.NetNS) error {
            contVeth, err := net.InterfaceByName(args.IfName)
            if err != nil {
                return err
            }
 
            for _, ipc := range result.IPs {
                if ipc.Version == "4" {
                    // 发送免费ARP
                    _ = arping.GratuitousArpOverIface(ipc.Address.IP, *contVeth)
                }
            }
            return nil
        }); err != nil {
            return err
        }
 
        // (此网桥作为容器的一个)网关模式下,为网桥配置IP地址,并启用IP转发
        if n.IsGW {
            var firstV4Addr net.IP
            var vlanInterface *current.Interface
            // 遍历IPAM分配的网关信息
            for _, gws := range []*gwInfo{gwsV4, gwsV6} {
                for _, gw := range gws.gws {
                    if gw.IP.To4() != nil && firstV4Addr == nil {
                        firstV4Addr = gw.IP
                    }
                    if n.Vlan != 0 {
                        // 如有必要,创建名为brname.vlanid的、网桥的VLAN子接口
                        vlanIface, err := ensureVlanInterface(br, n.Vlan)
                        if err != nil {
                            return fmt.Errorf("failed to create vlan interface: %v", err)
                        }
 
                        if vlanInterface == nil {
                            vlanInterface = &current.Interface{Name: vlanIface.Attrs().Name,
                                Mac: vlanIface.Attrs().HardwareAddr.String()}
                            // VLAN子接口添加到结果中
                            result.Interfaces = append(result.Interfaces, vlanInterface)
                        }
                        // 将网关IP地址分配给网桥的VLAN子接口
                        err = ensureAddr(vlanIface, gws.family, &gw, n.ForceAddress)
                        if err != nil {
                            return fmt.Errorf("failed to set vlan interface for bridge with addr: %v", err)
                        }
                    } else {
                        // 将IP分配给网桥
                        err = ensureAddr(br, gws.family, &gw, n.ForceAddress)
                        if err != nil {
                            return fmt.Errorf("failed to set bridge addr: %v", err)
                        }
                    }
                }
 
                if gws.gws != nil {
                    // 启用宿主机的IP转发功能
                    if err = enableIPForward(gws.family); err != nil {
                        return fmt.Errorf("failed to enable forwarding: %v", err)
                    }
                }
            }
        }
 
        if n.IPMasq {
            // 设置iptables规则,以实现MASQ
            // 生成一个自定义链
            chain := utils.FormatChainName(n.Name, args.ContainerID)
            comment := utils.FormatComment(n.Name, args.ContainerID)
            for _, ipc := range result.IPs {
                // 安装MASQ到chain,此china将位于NAT表作为POSTROUTING的子链
                if err = ip.SetupIPMasq(&ipc.Address, chain, comment); err != nil {
                    return err
                }
            }
        }
    }
 
    // 在第一个VETH添加之后,或者设置IP地址之后,网桥的MAC可能变化
    // 因此重新获取网桥
    br, err = bridgeByName(n.BrName)
    if err != nil {
        return err
    }
    brInterface.Mac = br.Attrs().HardwareAddr.String()
 
    result.DNS = n.DNS
 
    // Return an error requested by testcases, if any
    if debugPostIPAMError != nil {
        return debugPostIPAMError
    }
 
    success = true
 
    return types.PrintResult(result, cniVersion)
}
 
func cmdDel(args *skel.CmdArgs) error {
    n, _, err := loadNetConf(args.StdinData)
    if err != nil {
        return err
    }
 
    isLayer3 := n.IPAM.Type != ""
 
    if isLayer3 {
        // 调用IPAM插件,回收IP地址
        if err := ipam.ExecDel(n.IPAM.Type, args.StdinData); err != nil {
            return err
        }
    }
 
    if args.Netns == "" {
        return nil
    }
 
    // 网络命名空间存在,尝试清理其中的网络接口,由于可能调用多次
    // 因此需要实现幂等操作,如果网络接口已经被移除过,不要返回错误
    var ipnets []*net.IPNet
    err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
        var err error
        // 删除容器网络接口,导致VETH对被删除
        ipnets, err = ip.DelLinkByNameAddr(args.IfName)
        if err != nil && err == ip.ErrLinkNotFound {
            return nil
        }
        return err
    })
 
    if err != nil {
        return err
    }
 
    // 清理MASQ规则
    if isLayer3 && n.IPMasq {
        chain := utils.FormatChainName(n.Name, args.ContainerID)
        comment := utils.FormatComment(n.Name, args.ContainerID)
        for _, ipn := range ipnets {
            if err := ip.TeardownIPMasq(ipn, chain, comment); err != nil {
                return err
            }
        }
    }
 
    return err
}
 
// 这个插件就是基于skel做的,如果自己从头开发CNI插件,这是很好的参考
func main() {
    skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("bridge"))
}
 
type cniBridgeIf struct {
    Name        string
    ifIndex     int
    peerIndex   int
    masterIndex int
    found       bool
}
 
 
// CHECK接口
func cmdCheck(args *skel.CmdArgs) error {
    // 检查网络配置
    n, _, err := loadNetConf(args.StdinData)
    if err != nil {
        return err
    }
    netns, err := ns.GetNS(args.Netns)
    if err != nil {
        return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
    }
    defer netns.Close()
 
    // 调用IPAM插件进行检查
    err = ipam.ExecCheck(n.IPAM.Type, args.StdinData)
    if err != nil {
        return err
    }
 
    // 解析并转换上一次ADD调用的结果
    if n.NetConf.RawPrevResult == nil {
        return fmt.Errorf("Required prevResult missing")
    }
    if err := version.ParsePrevResult(&n.NetConf); err != nil {
        return err
    }
 
    result, err := current.NewResultFromResult(n.PrevResult)
    if err != nil {
        return err
    }
 
    var errLink error
    var contCNI, vethCNI cniBridgeIf
    var brMap, contMap current.Interface
 
    // 遍历上次调用结果中的网络接口列表,找到容器网络接口、网桥接口
    for _, intf := range result.Interfaces {
        if n.BrName == intf.Name {
            brMap = *intf
            continue
        } else if args.IfName == intf.Name {
            if args.Netns == intf.Sandbox {
                contMap = *intf
                continue
            }
        }
    }
    // 检查网桥
    brCNI, err := validateCniBrInterface(brMap, n)
    if err != nil {
        return err
    }
 
    // 检查容器的沙盒是否就是参数中的网络命名空间
    if args.Netns != contMap.Sandbox {
        return fmt.Errorf("Sandbox in prevResult %s doesn't match configured netns: %s",
            contMap.Sandbox, args.Netns)
    }
 
    // 检查容器网络接口
    if err := netns.Do(func(_ ns.NetNS) error {
        contCNI, errLink = validateCniContainerInterface(contMap)
        if errLink != nil {
            return errLink
        }
        return nil
    }); err != nil {
        return err
    }
 
    // 检查容器veth的对端
    for _, intf := range result.Interfaces {
        // Skip this result if name is the same as cni bridge
        // It's either the cni bridge we dealt with above, or something with the
        // same name in a different namespace.  We just skip since it's not ours
        if brMap.Name == intf.Name {
            continue
        }
 
        // same here for container name
        if contMap.Name == intf.Name {
            continue
        }
 
        vethCNI, errLink = validateCniVethInterface(intf, brCNI, contCNI)
        if errLink != nil {
            return errLink
        }
 
        if vethCNI.found {
            // veth with container interface as peer and bridge as master found
            break
        }
    }
 
    if !brCNI.found {
        return fmt.Errorf("CNI created bridge %s in host namespace was not found", n.BrName)
    }
    if !contCNI.found {
        return fmt.Errorf("CNI created interface in container %s not found", args.IfName)
    }
    if !vethCNI.found {
        return fmt.Errorf("CNI veth created for bridge %s was not found", n.BrName)
    }
 
    // 检查IP地址、路由、DNS,是否和容器中的状态匹配
    if err := netns.Do(func(_ ns.NetNS) error {
        // 检查容器IP是否符合期望
        err = ip.ValidateExpectedInterfaceIPs(args.IfName, result.IPs)
        if err != nil {
            return err
        }
        // 检查路由是否匹配
        err = ip.ValidateExpectedRoute(result.Routes)
        if err != nil {
            return err
        }
        return nil
    }); err != nil {
        return err
    }
 
    return nil
}
 
// 下面的若干函数都是由CHECK接口调用,实现了各方面的检查
 
// 检查网络接口
func validateInterface(intf current.Interface, expectInSb bool) (cniBridgeIf, netlink.Link, error) {
 
    ifFound := cniBridgeIf{found: false}
    if intf.Name == "" {
        return ifFound, nil, fmt.Errorf("Interface name missing ")
    }
    // 确保接口存在
    link, err := netlink.LinkByName(intf.Name)
    if err != nil {
        return ifFound, nil, fmt.Errorf("Interface name %s not found", intf.Name)
    }
    // 确保接口在/不在网络沙盒中
    if expectInSb {
        if intf.Sandbox == "" {
            return ifFound, nil, fmt.Errorf("Interface %s is expected to be in a sandbox", intf.Name)
        }
    } else {
        if intf.Sandbox != "" {
            return ifFound, nil, fmt.Errorf("Interface %s should not be in sandbox", intf.Name)
        }
    }
 
    return ifFound, link, err
}
 
// 检查网桥
func validateCniBrInterface(intf current.Interface, n *NetConf) (cniBridgeIf, error) {
 
    // 检查网桥接口本身
    brFound, link, err := validateInterface(intf, false)
    if err != nil {
        return brFound, err
    }
    // 断言其是一个网桥
    _, isBridge := link.(*netlink.Bridge)
    if !isBridge {
        return brFound, fmt.Errorf("Interface %s does not have link type of bridge", intf.Name)
    }
    // 如果网桥有MAC,断言它和结果中的MAC一致
    if intf.Mac != "" {
        if intf.Mac != link.Attrs().HardwareAddr.String() {
            return brFound, fmt.Errorf("Bridge interface %s Mac doesn't match: %s", intf.Name, intf.Mac)
        }
    }
    // 混杂模式断言
    linkPromisc := link.Attrs().Promisc != 0
    if linkPromisc != n.PromiscMode {
        return brFound, fmt.Errorf("Bridge interface %s configured Promisc Mode %v doesn't match current state: %v ",
            intf.Name, n.PromiscMode, linkPromisc)
    }
 
    brFound.found = true
    brFound.Name = link.Attrs().Name
    brFound.ifIndex = link.Attrs().Index
    brFound.masterIndex = link.Attrs().MasterIndex
 
    return brFound, nil
}
 
// 检查容器中的VETH接口
func validateCniVethInterface(intf *current.Interface, brIf cniBridgeIf, contIf cniBridgeIf) (cniBridgeIf, error) {
    // 同样的,首先进行接口通用检查
    vethFound, link, err := validateInterface(*intf, false)
    if err != nil {
        return vethFound, err
    }
    // 断言其是一个VETH
    _, isVeth := link.(*netlink.Veth)
    if !isVeth {
        // just skip it, it's not what CNI created
        return vethFound, nil
    }
    // 断言对端的序号符合预期
    _, vethFound.peerIndex, err = ip.GetVethPeerIfindex(link.Attrs().Name)
    if err != nil {
        return vethFound, fmt.Errorf("Unable to obtain veth peer index for veth %s", link.Attrs().Name)
    }
    vethFound.ifIndex = link.Attrs().Index
    vethFound.masterIndex = link.Attrs().MasterIndex
 
    if vethFound.ifIndex != contIf.peerIndex {
        return vethFound, nil
    }
 
    if contIf.ifIndex != vethFound.peerIndex {
        return vethFound, nil
    }
 
    if vethFound.masterIndex != brIf.ifIndex {
        return vethFound, nil
    }
    // 断言MAC地址符合预期
    if intf.Mac != "" {
        if intf.Mac != link.Attrs().HardwareAddr.String() {
            return vethFound, fmt.Errorf("Interface %s Mac doesn't match: %s not found", intf.Name, intf.Mac)
        }
    }
 
    vethFound.found = true
    vethFound.Name = link.Attrs().Name
 
    return vethFound, nil
}
 
func validateCniContainerInterface(intf current.Interface) (cniBridgeIf, error) {
 
    vethFound, link, err := validateInterface(intf, true)
    if err != nil {
        return vethFound, err
    }
 
    _, isVeth := link.(*netlink.Veth)
    if !isVeth {
        return vethFound, fmt.Errorf("Error: Container interface %s not of type veth", link.Attrs().Name)
    }
    _, vethFound.peerIndex, err = ip.GetVethPeerIfindex(link.Attrs().Name)
    if err != nil {
        return vethFound, fmt.Errorf("Unable to obtain veth peer index for veth %s", link.Attrs().Name)
    }
    vethFound.ifIndex = link.Attrs().Index
 
    if intf.Mac != "" {
        if intf.Mac != link.Attrs().HardwareAddr.String() {
            return vethFound, fmt.Errorf("Interface %s Mac %s doesn't match container Mac: %s", intf.Name, intf.Mac, link.Attrs().HardwareAddr)
        }
    }
 
    vethFound.found = true
    vethFound.Name = link.Attrs().Name
 
    return vethFound, nil
}
VETH对创建

创建VETH对的过程如下:

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// 从容器命名空间中调用此函数
// 自动将一端移动到“宿主机”命名空间(实际上是任何NS,不一定是真正的宿主机NS)
func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (hostVeth net.Interface, containerVeth net.Interface, error) {
    return SetupVethWithName(contVethName, "", mtu, hostNS)
}
 
func SetupVethWithName(contVethName, hostVethName string, mtu int, hostNS ns.NetNS) (net.Interface, net.Interface, error) {
    // 宿主端      容器端
    hostVethName, contVeth, err := makeVeth(contVethName, hostVethName, mtu)
    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)
    }
 
    hostVeth, err := netlink.LinkByName(hostVethName)
    if err != nil {
        return net.Interface{}, net.Interface{}, fmt.Errorf("failed to lookup %q: %v", hostVethName, err)
    }
 
    // 移动对端到宿主机命名空间
    if err = netlink.LinkSetNsFd(hostVeth, int(hostNS.Fd())); err != nil {
        return net.Interface{}, net.Interface{}, fmt.Errorf("failed to move veth to host netns: %v", err)
    }
 
    // 在宿主机命名空间启动对端
    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
}
 
func makeVeth(name, vethPeerName string, mtu int) (peerName string, veth netlink.Link, err error) {
    for i := 0; i < 10; i++ {
        if vethPeerName != "" {
            peerName = vethPeerName
        } else {
            // 给对端随机分配名称
            peerName, err = RandomVethName()
            if err != nil {
                return
            }
        }
 
        veth, err = makeVethPair(name, peerName, mtu)
        switch {
        case err == nil:
            return
 
        case os.IsExist(err):
            if peerExists(peerName) && vethPeerName == "" {
                continue
            }
            err = fmt.Errorf("container veth name provided (%v) already exists", name)
            return
 
        default:
            err = fmt.Errorf("failed to make veth pair: %v", err)
            return
        }
    }
 
    // should really never be hit
    err = fmt.Errorf("failed to find a unique veth name")
    return
}
 
 
func makeVethPair(name, peer string, mtu int) (netlink.Link, error) {
    // 创建VETH结构
    veth := &netlink.Veth{
        LinkAttrs: netlink.LinkAttrs{
            Name:  name,
            Flags: net.FlagUp,
            MTU:   mtu,
        },
        // 对端(需要移动到宿主机的)名称,随机,例如veth99350820
        PeerName: peer,
    }
    // 添加连接
    if err := netlink.LinkAdd(veth); err != nil {
        return nil, err
    }
    // 创建连接后需要重新获取
    // 因为索引、MAC等信息可能更新
    veth2, err := netlink.LinkByName(name)
    if err != nil {
        netlink.LinkDel(veth) // 出错则删除设备并清理
        return nil, err
    }
 
    return veth2, nil
}
网络拓扑

使用配置:

JSON
1
2
3
4
5
6
7
8
{
    "cniVersion": "0.3.1",
    "name": "testConfig",
    "type": "bridge",
    "bridge": "bridge0",
    "vlan": 100,
    "ipam": {}
}

获得应答:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
    "cniVersion": "0.3.1",
    "interfaces": [
        {
            "name": "bridge0",
            "mac": "ce:d7:70:c7:97:08"
        },
        {
            "name": "veth99350820",
            "mac": "ce:d7:70:c7:97:08"
        },
        {
            "name": "eth0",
            "mac": "ce:87:b0:d4:94:c3",
            "sandbox": "/var/run/netns/cnitest-cdf173b0-fa2a-66ab-e5d6-bba23a0708f5"
        }
    ],
    "dns": {}
}

产生的网络拓扑如下: 

bridge-vlan

← 通过WebAssembly扩展Envoy
CRI学习笔记 →

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Related Posts

  • 基于Calico的CNI
  • Galaxy学习笔记
  • Cilium学习笔记
  • Flannel学习笔记
  • 基于Helm的Kubernetes资源管理

Recent Posts

  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • A Comprehensive Study of Kotlin for Java Developers
  • 背诵营笔记
  • 利用LangChain和语言模型交互
  • 享学营笔记
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注容器方向。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
    In this blog post, I will walk ...
  • A Comprehensive Study of Kotlin for Java Developers
    Introduction Purpose of the Study Understanding the Mo ...
  • 背诵营笔记
    Day 1 Find Your Greatness 原文 Greatness. It’s just ...
  • 利用LangChain和语言模型交互
    LangChain是什么 从名字上可以看出来,LangChain可以用来构建自然语言处理能力的链条。它是一个库 ...
  • 享学营笔记
    Unit 1 At home Lesson 1 In the ...
  • K8S集群跨云迁移
    要将K8S集群从一个云服务商迁移到另外一个,需要解决以下问题: 各种K8S资源的迁移 工作负载所挂载的数 ...
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
  • Galaxy学习笔记
    简介 Galaxy是TKEStack的一个网络组件,支持为TKE集群提供Overlay/Underlay容器网 ...
TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 90 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • Bazel学习笔记 37 people like this
  • PhoneGap学习笔记 32 people like this
  • NaCl学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • Ceph学习笔记 27 people like this
  • 基于Calico的CNI 27 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • qg on Istio中的透明代理问题
  • heao on 基于本地gRPC的Go插件系统
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • haolipeng on 基于本地gRPC的Go插件系统
  • 吴杰 on 基于C/C++的WebSocket库
©2005-2025 Gmem.cc | Powered by WordPress | 京ICP备18007345号-2