许吉友 - 运维

安装 cnitool 二进制命令:

$ go get github.com/containernetworking/cni
$ go install github.com/containernetworking/cni/cnitool

然后把 $GOPATH/go/bin 中的 cnitool 拷贝到 /usr/bin

编译官方提供的全部 cni 插件:

$ git clone https://github.com/containernetworking/plugins.git
$ cd plugins
$ ./build_linux.sh

执行完成后,会在当前目录生成一个 bin 目录,里面存有好多二进制的 cni 插件。

然后编写配置文件(这里官方文档是错误的):

$ cat /etc/cni/net.d/10-myptp.conflist
{
    "cniVersion": "0.4.0",
    "name": "myptp",
    "plugins": [
        {
            "type": "ptp",
            "ipMasq": true,
            "ipam": {
                "type": "host-local",
                "subnet": "172.16.29.0/24",
                "routes": [
                    {
                        "dst": "0.0.0.0/0"
                    }
                ]
             }
        }
    ]
}

ptp 插件用于创建 veth 设备对,关于 veth 设备对可以看: 一次网络命令实践.md

创建一个网络命名空间:

$ sudo ip netns add testing

在刚刚创建的命名空间内执行插件:

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

检查效果 (ONLY for spec v0.4.0+)(亲测无效):

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

测试:

$ sudo ip -n testing addr
4: eth0@if528: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 22:77:81:25:4e:54 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.16.29.2/24 brd 172.16.29.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::2077:81ff:fe25:4e54/64 scope link 
       valid_lft forever preferred_lft forever

这条命令会列出网络命名空间 testing 内的网卡,我这里看到了一个名为 eth0@if528 的网卡

然后查看本机原始命名空间内的网卡:

$ ip a
528: vethbb097e6c@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 32:b3:c1:20:2d:b3 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.16.29.1/32 brd 172.16.29.1 scope global vethbb097e6c
       valid_lft forever preferred_lft forever

可以看到两个命名空间内的两个网卡遥相呼应。

测试 testing 命名空间内的网卡是否可以工作:

$ sudo ip netns exec testing ping -c 4 www.baidu.com

实测是可以工作的,ping 的 -c 参数指定了发包次数。

清理:

$ sudo CNI_PATH=./bin cnitool del myptp /var/run/netns/testing
$ sudo ip netns del testing

上面已经测试了 ptp 插件。下面开始想办法开发一个自己的插件,并运行起来。

上面在官方的插件代码里,有一个示例插件,在 plugins/sample 包下,下面先编译这个包,让后将其放入 bin 目录(先不该动任何代码):

$ cd plugins/sample/
$ go build 
$ cd ../../
$ cp plugins/sample/sample ./bin

然后编写配置文件,在 plugins/sample/sample_linux_test.go 这个文件中有配置,需要对其进行改造:

$ cat /etc/cni/net.d/10-sample.conflist
{
    "cniVersion": "0.4.0",
    "name": "sample",
    "plugins": [
        {
            "type": "sample",
            "anotherAwesomeArg": "haha",
            "prevResult": {
                        "interfaces": [
                                 {
                                          "name": "eth0",
                                          "sandbox": "/var/run/netns/test"
                                 }
                         ],
                         "ips": [
                                 {
                                         "version": "4",
                                         "address": "10.0.0.2/24",
                                         "gateway": "10.0.0.1",
                                         "interface": 0
                                 }
                         ],
                         "routes": []
                 }
        }
    ]
}

然后使用 cnitool 来执行 sample 插件:

$ sudo ip netns add test
$ sudo CNI_PATH=./bin cnitool add sample /var/run/netns/test

测试:

$ sudo ip -n test addr

OK,这样 就可以自定义 CNI 插件了。


cnitool 源码分析

cnitool 的源码在 https://github.com/containernetworking/cni/blob/master/cnitool/cnitool.go

一共就100多行,很简单。

其中,最关键的几句话:

  cninet := libcni.NewCNIConfig(filepath.SplitList(os.Getenv(EnvCNIPath)), nil)

    rt := &libcni.RuntimeConf{
        ContainerID:    containerID,
        NetNS:          netns,
        IfName:         ifName,
        Args:           cniArgs,
        CapabilityArgs: capabilityArgs,
    }

    switch os.Args[1] {
    case CmdAdd:
        result, err := cninet.AddNetworkList(context.TODO(), netconf, rt)
        if result != nil {
            _ = result.Print()
        }
        exit(err)
    case CmdCheck:
        err := cninet.CheckNetworkList(context.TODO(), netconf, rt)
        exit(err)
    case CmdDel:
        exit(cninet.DelNetworkList(context.TODO(), netconf, rt))
    }

可以看出,其实就是传入配置,然后调用的插件的三个方法。。。