深入理解CNI(容器網(wǎng)絡接口)

CNI簡介

CNI(Container Network Interface)就是這樣的一個接口層,它定義了一套接口標準,提供了規(guī)范文檔以及一些標準實現(xiàn)。采用CNI規(guī)范來設置容器網(wǎng)絡的容器平臺不需要關注網(wǎng)絡的設置的細節(jié),只需要按CNI規(guī)范來調用CNI接口即可實現(xiàn)網(wǎng)絡的設置。
CNI最初是由CoreOS為rkt容器引擎創(chuàng)建的,隨著不斷發(fā)展,已經(jīng)成為事實標準。目前絕大部分的容器平臺都采用CNI標準(rkt,Kubernetes,OpenShift等)。本篇內(nèi)容基于CNI最新的發(fā)布版本v0.4.0。值得注意的是,Docker并沒有采用CNI標準,而是在CNI創(chuàng)建之初同步開發(fā)了CNM(Container Networking Model)標準。但由于技術和非技術原因,CNM模型并沒有得到廣泛的應用。CNI是怎么工作的

bandwidth bridge dhcp firewall flannel host-device host-local ipvlan loopback macvlan portmap ptp sbr static tuning vlan

CNI通過JSON格式的配置文件來描述網(wǎng)絡配置,當需要設置容器網(wǎng)絡時,由容器運行時負責執(zhí)行CNI插件,并通過CNI插件的標準輸入(stdin)來傳遞配置文件信息,通過標準輸出(stdout)接收插件的執(zhí)行結果。圖中的 libcni 是CNI提供的一個go package,封裝了一些符合CNI規(guī)范的標準操作,便于容器運行時和網(wǎng)絡插件對接CNI標準。
舉一個直觀的例子,假如我們要調用bridge插件將容器接入到主機網(wǎng)橋,則調用的命令看起來長這樣:# CNI_COMMAND=ADD 顧名思義表示創(chuàng)建。
# XXX=XXX 其他參數(shù)定義見下文。
# < config.json 表示從標準輸入傳遞配置文件
CNI_COMMAND=ADD XXX=XXX ./bridge < config.json
插件入?yún)?/strong>
容器運行時通過設置環(huán)境變量以及從標準輸入傳入的配置文件來向插件傳遞參數(shù)。
環(huán)境變量
-
CNI_COMMAND:定義期望的操作,可以是ADD,DEL,CHECK或VERSION。
-
CNI_CONTAINERID:容器ID,由容器運行時管理的容器唯一標識符。
-
CNI_NETNS:容器網(wǎng)絡命名空間的路徑。(形如 /run/netns/[nsname])。
-
CNI_IFNAME:需要被創(chuàng)建的網(wǎng)絡接口名稱,例如eth0。
-
CNI_ARGS:運行時調用時傳入的額外參數(shù),格式為分號分隔的key-value對,例如FOO=BAR;ABC=123
-
CNI_PATH:CNI插件可執(zhí)行文件的路徑,例如/opt/cni/bin。
配置文件
文件示例:{
"cniVersion": "0.4.0", // 表示希望插件遵循的CNI標準的版本。
"name": "dbnet", // 表示網(wǎng)絡名稱。這個名稱并非指網(wǎng)絡接口名稱,是便于CNI管理的一個表示。應當在當前主機(或其他管理域)上全局唯一。
"type": "bridge", // 插件類型
"bridge": "cni0", // Bridge插件的參數(shù),指定網(wǎng)橋名稱。
"ipam": { // IP Allocation Management,管理IP地址分配。
"type": "host-local", // IPAM插件的類型。
// IPAM定義的參數(shù)
"subnet": "10.1.0.0/16",
"gateway": "10.1.0.1"
}
} 公共定義部分:
配置文件分為公共部分和插件定義部分。公共部分在CNI項目中使用結構體NetworkConfig定義:type NetworkConfig struct {
Network *types.NetConf
Bytes []byte
}
...
// NetConf describes a network.
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:"-"`
}
-
cniVersion:表示希望插件遵循的CNI標準的版本。
-
name:表示網(wǎng)絡名稱。這個名稱并非指網(wǎng)絡接口名稱,是便于CNI管理的一個表示。應當在當前主機(或其他管理域)上全局唯一。
-
type:表示插件的名稱,也就是插件對應的可執(zhí)行文件的名稱。
-
Bridge:該參數(shù)屬于bridge插件的參數(shù),指定主機網(wǎng)橋的名稱。
-
IPAM:表示IP地址分配插件的配置,ipam.type則表示IPAM的插件類型。
更詳細的信息,可以參考官方文檔:https://github.com/containernetworking/cni/blob/spec-v0.4.0/SPEC.md#network-configuration
插件定義部分:
上文提到,配置文件最終是傳遞給具體的CNI插件的,因此插件定義部分才是配置文件的“完全體”。公共部分定義只是為了方便各插件將其嵌入到自身的配置文件定義結構體中,舉Bridge插件為例:type NetConf struct {
types.NetConf // <-- 嵌入公共部分
// 底下的都是插件定義部分
BrName string `json:"bridge"`
IsGW bool `json:"isGateway"`
IsDefaultGW bool `json:"isDefaultGateway"`
ForceAddress bool `json:"forceAddress"`
IPMasq bool `json:"ipMasq"`
MTU int `json:"mtu"`
HairpinMode bool `json:"hairpinMode"`
PromiscMode bool `json:"promiscMode"`
Vlan int `json:"vlan"`
Args struct {
Cni BridgeArgs `json:"cni,omitempty"`
} `json:"args,omitempty"`
RuntimeConfig struct {
Mac string `json:"mac,omitempty"`
} `json:"runtimeConfig,omitempty"`
mac string
}
插件操作類型
CNI插件的操作類型只有四種:ADD,DEL,CHECK和VERSION。插件調用者通過環(huán)境變量CNI_COMMAND來指定需要執(zhí)行的操作。
ADD
ADD操作負責將容器添加到網(wǎng)絡,或對現(xiàn)有的網(wǎng)絡設置做更改。具體地說,ADD操作要么:
-
為容器所在的網(wǎng)絡命名空間創(chuàng)建一個網(wǎng)絡接口,或者
-
修改容器所在網(wǎng)絡命名空間中的指定網(wǎng)絡接口
例如通過ADD將容器網(wǎng)絡接口接入到主機的網(wǎng)橋中。
其中網(wǎng)絡接口名稱由CNI_IFNAME指定,網(wǎng)絡命名空間由CNI_NETNS指定。
DEL
DEL操作負責從網(wǎng)絡中刪除容器,或取消對應的修改,可以理解為是ADD的逆操作。具體地說,DEL操作要么:
-
為容器所在的網(wǎng)絡命名空間刪除一個網(wǎng)絡接口,或者
-
撤銷ADD操作的修改
例如通過DEL將容器網(wǎng)絡接口從主機網(wǎng)橋中刪除。
其中網(wǎng)絡接口名稱由CNI_IFNAME指定,網(wǎng)絡命名空間由CNI_NETNS指定。
CHECK
CHECK操作是v0.4.0加入的類型,用于檢查網(wǎng)絡設置是否符合預期。容器運行時可以通過CHECK來檢查網(wǎng)絡設置是否出現(xiàn)錯誤,當CHECK返回錯誤時(返回了一個非0狀態(tài)碼),容器運行時可以選擇Kill掉容器,通過重新啟動來重新獲得一個正確的網(wǎng)絡配置。
VERSION
VERSION操作用于查看插件支持的版本信息。$ CNI_COMMAND=VERSION /opt/cni/bin/bridge
{"cniVersion":"0.4.0","supportedVersions":["0.1.0","0.2.0","0.3.0","0.3.1","0.4.0"]}
鏈式調用
單個CNI插件的職責是單一的,比如Bridge插件負責網(wǎng)橋的相關配置, Firewall插件負責防火墻相關配置, Portmap插件負責端口映射相關配置。因此,當網(wǎng)絡設置比較復雜時,通常需要調用多個插件來完成。CNI支持插件的鏈式調用,可以將多個插件組合起來,按順序調用。例如先調用Bridge插件設置容器IP,將容器網(wǎng)卡與主機網(wǎng)橋連通,再調用Portmap插件做容器端口映射。容器運行時可以通過在配置文件設置Plugins數(shù)組達到鏈式調用的目的:{
"cniVersion": "0.4.0",
"name": "dbnet",
"plugins": [
{
"type": "bridge",
// type (plugin) specific
"bridge": "cni0"
},
"ipam": {
"type": "host-local",
// ipam specific
"subnet": "10.1.0.0/16",
"gateway": "10.1.0.1"
}
},
{
"type": "tuning",
"sysctl": {
"net.core.somaxconn": "500"
}
}
]
}
Name string
CNIVersion string
DisableCheck bool
Plugins []*NetworkConfig
Bytes []byte
}
"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"
}
}
"cniVersion": "0.4.0",
"name": "dbnet",
"type": "tuning",
"sysctl": {
"net.core.somaxconn": "500"
},
"prevResult": { // 調用Bridge插件的返回結果
...
}
}

下載CNI插件
為方便起見,我們直接下載可執(zhí)行文件:wget https://github.com/containernetworking/plugins/releases/download/v0.9.1/cni-plugins-linux-amd64-v0.9.1.tgz
mkdir -p ~/cni/bin
tar zxvf cni-plugins-linux-amd64-v0.9.1.tgz -C ./cni/bin
chmod x ~/cni/bin/*
ls ~/cni/bin/
bandwidth bridge dhcp firewall flannel host-device host-local ipvlan loopback macvlan portmap ptp sbr static tuning vlan vrfz
示例1——調用單個插件
在示例1中,我們會直接調用CNI插件,為容器設置eth0接口,為其分配IP地址,并接入主機網(wǎng)橋mynet0。跟Docker默認使用的使用網(wǎng)絡模式一樣,只不過我們將docker0換成了mynet0。啟動容器
雖然Docker不使用CNI規(guī)范,但可以通過指定--net=none的方式讓Docker不設置容器網(wǎng)絡。以Nginx鏡像為例:contid=$(docker run -d --net=none --name nginx nginx) # 容器ID
pid=$(docker inspect -f '{{ .State.Pid }}' $contid) # 容器進程ID
netnspath=/proc/$pid/ns/net # 命名空間路徑
1: lo:
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
接下來我們使用Bridge插件為容器創(chuàng)建網(wǎng)絡接口,并連接到主機網(wǎng)橋。創(chuàng)建bridge.json配置文件,內(nèi)容如下:{
"cniVersion": "0.4.0",
"name": "mynet",
"type": "bridge",
"bridge": "mynet0",
"isDefaultGateway": true,
"forceAddress": false,
"ipMasq": true,
"hairpinMode": true,
"ipam": {
"type": "host-local",
"subnet": "10.10.0.0/16"
}
}
"cniVersion": "0.4.0",
"interfaces": [
....
],
"ips": [
{
"version": "4",
"interface": 2,
"address": "10.10.0.2/16", //給容器分配的IP地址
"gateway": "10.10.0.1"
}
],
"routes": [
.....
],
"dns": {}
}
1: lo:
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
5: eth0@if40:
link/ether c2:8f:ea:1b:7f:85 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.10.0.2/16 brd 10.10.255.255 scope global eth0
valid_lft forever preferred_lft forever
10.10.0.2 last_reserved_ip.0 lock
從主機訪問驗證
由于mynet0是我們添加的網(wǎng)橋,還未設置路由,因此驗證前我們需要先為容器所在的網(wǎng)段添加路由:ip route add 10.10.0.0/16 dev mynet0 src 10.10.0.1 # 添加路由
curl -I 10.10.0.2 # IP換成實際分配給容器的IP地址
HTTP/1.1 200 OK
....
刪除容器網(wǎng)絡接口
刪除的調用入?yún)⒏砑拥娜雲(yún)⑹且粯拥模薈NI_COMMAND要替換成DEL:CNI_COMMAND=DEL CNI_CONTAINERID=$contid CNI_NETNS=$netnspath CNI_IFNAME=eth0 CNI_PATH=~/cni/bin ~/cni/bin/bridge < bridge.json
在示例2中,我們將在示例1的基礎上,使用Portmap插件為容器添加端口映射。
使用cnitool工具
前面的介紹中,我們知道在鏈式調用過程中,調用方需要轉換配置文件,并需要將上一次插件的返回結果插入到本次插件的配置文件中。這是一項繁瑣的工作,而libcni已經(jīng)將這些過程封裝好了,在示例2中,我們將使用基于 libcni的命令行工具cnitool來簡化這些操作。
示例2將復用示例1中的容器,因此在開始示例2時,請確保已刪除示例1中的網(wǎng)絡接口。
通過源碼編譯或go install來安裝cnitool:go install github.com/containernetworking/cni/cnitool@latest
配置文件
libcni會讀取.conflist后綴的配置文件,我們在當前目錄創(chuàng)建portmap.conflist:{
"cniVersion": "0.4.0",
"name": "portmap",
"plugins": [
{
"type": "bridge",
"bridge": "mynet0",
"isDefaultGateway": true,
"forceAddress": false,
"ipMasq": true,
"hairpinMode": true,
"ipam": {
"type": "host-local",
"subnet": "10.10.0.0/16",
"gateway": "10.10.0.1"
}
},
{
"type": "portmap",
"runtimeConfig": {
"portMappings": [
{"hostPort": 8080, "containerPort": 80, "protocol": "tcp"}
]
}
}
]
}
設置容器網(wǎng)絡
使用cnitool我們還需要設置兩個環(huán)境變量:
-
NETCONFPATH:指定配置文件(*.conflist)的所在路徑,默認路徑為/etc/cni/net.d
-
CNI_PATH:指定CNI插件的存放路徑。
刪除網(wǎng)絡配置
使用cnitool del命令刪除容器網(wǎng)絡:CNI_PATH=~/cni/bin NETCONFPATH=. cnitool del portmap $netnspath

-
通過JSON配置文件定義網(wǎng)絡配置;
-
通過調用可執(zhí)行程序(CNI插件)來對容器網(wǎng)絡執(zhí)行配置;
-
通過鏈式調用的方式來支持多插件的組合使用。