cnitool -- your CNI Swiss Army knife

If you’re looking at developing (or debugging!) CNI plugins, you’re going to need a workflow for developing CNI plugins – something that really lets you get in there, and see exactly what a CNI plugin is doing. You’re going to need a bit of a swiss army knife, or something that slices, dices, and makes juilienne fries. cnitool is just the thing to do the job. Today we’ll walk through setting up cnitool, and then we’ll make a “dummy” CNI plugin to use it with, and we’ll run a reference CNI plugin.

We’ll also cover some of the basics of the information that’s passed to and from the CNI plugins and CNI itself, and how you might interact with that information, and how you might inspect a container that’s been plumbed with interfaces as created by a CNI plugin.

In this article, we’ll do this entirely without interacting with Kubernetes (and save it for another time!). And we actually do it without a container runtime at all – no docker, no crio. We just create the network namespace by hand. But the same kind of principles apply with both a container runtime (docker, crio) or a container orchestration enginer (e.g. k8s)

You might remember my blog article about a workflow for developing CNI plugins. That article uses the, which is still totally valid. You might look at it for a reference, but CNI tool gives a bit more granularity.


Setting up cnitool and the reference CNI plugins.

Basically, all the steps necessary to install cnitool are available in the cnitool README. I’ll summarize them here, but, it may be worth a reference.

Install cnitool…

go get
go install

You can test if it’s in your path and operational with:

cnitool --help

Next, we’ll compile the “reference CNI plugins” – these are a series of plugins that are offered by the CNI maintainers that create network interfaces for pods (as well as provide a number of “meta” type plugins that alter the properties, attributes, and what not of a particular container’s network). We also set our CNI_PATH variable (which is used by cnitool to know where these plugin executables are)

git clone
cd plugins
export CNI_PATH=$(pwd)/bin
echo $CNI_PATH

Alright, you’re basically all setup at this point.

Creating a netns and running cnitool against it

We’ll need to create a CNI configuration. For testing purposes, we’re going to create a configuration for the bridge CNI.

Create a directory and file at /tmp/cniconfig/10-myptp.conf with these contents:

  "cniVersion": "0.4.0",
  "name": "myptp",
  "type": "ptp",
  "ipMasq": true,
  "ipam": {
    "type": "host-local",
    "subnet": "",
    "routes": [{
      "dst": ""

And then set your CNI configuration directory by exporting this variable as:

export NETCONFPATH=/tmp/cniconfig/

First we create a netns – a network namespace. This is kind of a privately sorta-jailed space in which network components live, and is the basis of networking in containers, “here’s your private namespace in which to do your network-y things”. This, from a CNI point of view, is equivalent to the “sandbox” which is the basis container of pods that run in kubernetes. In k8s we’d have one or more containers running inside this sandbox, and they’d share the networks as in this network namespace.

sudo ip netns add myplayground

You can go and list them to see that it’s there…

sudo ip netns list | grep myplayground

Now we’re going to run cnitool with sudo so it has the appropriate permissions, and we’re going to need to pass it along our environment variables and our path to cnitool (if your root user doesn’t have a go environment, or isn’t configured that way), for me it looks like:

sudo NETCONFPATH=$(echo $NETCONFPATH) CNI_PATH=$(echo $CNI_PATH) $(which cnitool) add myptp /var/run/netns/myplayground

Let’s breakdown what this is doing more or less…

You should get some output that looks like:

    "cniVersion": "0.4.0",
    "interfaces": [
            "name": "veth20b2acac",
            "mac": "62:22:15:72:b2:29"
            "name": "eth0",
            "mac": "42:48:16:0b:e9:98",
            "sandbox": "/var/run/netns/myplayground"
    "ips": [
            "version": "4",
            "interface": 1,
            "address": "",
            "gateway": ""
    "routes": [
            "dst": ""
    "dns": {}

You can then actually do a ping out that interface, with:

sudo ip -n myplayground addr
sudo ip netns exec myplayground ping -c 1

And you can use nsenter to more interactively play with it, too…

sudo nsenter --net=/var/run/netns/myplayground /bin/bash
[root@host dir]# ip a
[root@host dir]# ip route
[root@host dir]# ping -c 5

Let’s interactively look at a CNI plugin running with cnitool.

What we’re going to do is create a shell script that is a CNI plugin. You see, CNI plugins can be executables of any variety – they just need to be able to read from stdin, and write to stdout and stderr.

This is kind of a blank slate for a CNI plugin that’s made with bash. You could use this approach, but, in reality – you’ll probably write these applications with go. Why? Well, especially because there’s the CNI libraries (especially libcni) which you would use to be able to express some of these ideas about CNI in a more elegant fashion. Take a look at how Multus uses CNI’s skel (skeletal components, for the framework of your CNI plugin) in its main routine to call the methods as CNI has called them. Just read through Multus’ main.go and look how it imports skel and then using skel calls our method to add when CNI ADD is used.

First, let’s make a cni configuration for our dummy plugin. I made mine at /tmp/cniconfig/05-dummy.conf.

  "cniVersion": "0.4.0",
  "name": "mydummy",
  "type": "dummy"

There’s not a lot to pay attention to here, the most important things are:

Now, in the path where we have our reference CNI plugins, lets add another file, name it dummy, and then make sure its executable. In my case I did a:

vi ./bin/dummy
chmod 0755 ./bin/dummy

I made mine with the contents from this gist.

The first thing to note is that the majority of this file is to actually just setup some logging for looking at the CNI parameters, and all the magic happens in the last 3-4 lines.

Mainly, we want to output 3 environment using these three lines. These are some environment variables that are sent to us from CNI and that a CNI plugin can use to figure out the netns, the container id, and the CNI command.

Importantly – since we have this DEBUG variable turned on, we’re outputting via stderr… if there’s any stderr output during a CNI plugin run, this is considered a failure, as that’s what you’re supposed to do when you error out, is output to stderr.

And last but not least, we output a CNI result at the bottom line, which calls this function which outputs a (sorta kinda realistic) CNI result.

You can turn that off, but we have it on for demonstrative purposes so you can easily see the what those variables are.

So, let’s run it!

sudo NETCONFPATH=$(echo $NETCONFPATH) CNI_PATH=$(echo $CNI_PATH) $(which cnitool) add mydummy /var/run/netns/dummyplayground

And you can see output that looks like:

CNI method: ADD
CNI container id: cnitool-06764c511c35893f831e
CNI netns: /var/run/netns/dummyplayground
    "cniVersion": "0.4.0",
    "interfaces": [
            "name": "dummy"
    "dns": {}

Here we’ll see that there’s a lot of information that we as humans already know, since we’re executing CNI tool, but it demonstrates how a CNI plugin interacts with this information, it’s telling us that it:

These are the general basics of what a CNI plugin needs in order to operate. And then… from there, the sky’s the limit. A more realistic plugin might

And to learn a bit more, you might think about looking at some of the reference CNI plugins, and see what they do to create interfaces inside these network namespaces.

But what if my CNI plugins interacts with Kubernetes!?

…And that’s for next time! You’ll need a Kubernetes environment of some sort.