Preface
It's a series post about the Container Network Interface and you can find other posts below. [Container Network Interface] Bridge Network In Docker [Container Network Interface] CNI Introduction
In this post, I will show how to write your own CNI program.
Container Network Interface(CNI) can be implemented by any programming languages as you like.
You just to follow the interface and your program can be used for every infrastructure using the CNI for their network connectivity.
In this tutorial, I will use the golang
to implement a simple CNI witch create a Linux Bridge
in the host and connect the container and the host itself.
I have create a github repo for this tutorial and you can find it on hwchiu CNI_Tutorial_2018
Introduction
In order to help the develop to develop their own CNI, the CNCF
had setup two projects for developers.
Those projects are based on the golang language and provide useful libraries for the developer to control the Linux network functions, such as IP
, netlink
and network namespace
.
The ContainerNetworking/CNI provides the basic function for CNI implementation in golang and you can see the introduction of that project in its README
As well as the specification, this repository contains the Go source code of a library for integrating CNI into applications and an example command-line tool for executing CNI plugins. A separate repository contains reference plugins and a template for making new plugins.
The other project ContainerNetwokring/Plugins provides some basic network functions for your CNI and it can be divided into two types.
Basic CNI
It provides some basic CNI, such as Bridge
, MacVlan
, Host Device
.. And so on.
You can chain those CNI into your own CNI and combine those into a more powerful CNI.
IPAM
IPAM (IP Address Management) provides some method to handle the IP/Route management. It provides host-local
, dhcp
and static
three methods now.
In the host-local
, you just need to provide a configuration file to describe what subnet/gateway you want to use and it will allocate a unused IP address from that subnet for your CNI.
And the dhcp
will runs a DHCP
client in each container and send a dhcp request
to get a IP address from the dhcp
server.
In this tutorial, we will implement a bridge
CNI and explain those functions step by step.
Before We Start
Before we start to implement the CNI, we must know the interface
/specification
of the CNI
.
- Your CNI will be invoked when the container is
ready to create or has been terminated
.
- Allocate resources for the container, including the
IP address
and the network connectivity. - Remove all resources you allocated before when a container has been terminated.
- The caller will pass the following information into your CNI program
- Command (What kind of the event you should care)
- ADD
- DELETE
- VERSION
- ContainerID (The target ContainerID)
- NetNS (THe network namespace path of the container)
- IFNAME (The interface name should be created in the container)
- PATH (The current working PATH, you should use it to execute other CNI)
- STDIN (The configuration file of your CNI)
Step By Step
For each step, you can find a corresponding folder in my github repo and there's all golang files for each steps.
Step1
First, we need to provide two function for ADD
and DELETE
event which is used to allocate/recycle resource when the container has been start/terminated.
We use the framework provided by the The ContainerNetworking/CNI and it will encapsulate
Package main
Import (
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/version"
)
func cmdAdd(args *skel.CmdArgs) error {
return nil
}
func cmdDel(args *skel.CmdArgs) error {
return nil
}
func main() {
skel.PluginMain(cmdAdd, cmdDel, version.All)
}
In this framework, it encapsulates all information we need into a predefined type skel.CmdArgs
type CmdArgs struct {
ContainerID string
Netns string
IfName string
Args string
Path string
StdinData []byte
}
Use the go build
to build the binary and assume our execution file is example
and then we should provide a basic configuration which should contains useful information for our CNI.
Maybe we call the file configuration
its contents looks like
{
"name": "mynet",
"BridgeName": "test",
"IP": "192.0.2.1/24"
}
Now, We can use the following command to execute our CNI program.
sudo CNI_COMMAND=ADD CNI_CONTAINERID=ns1 \
CNI_NETNS=/var/run/netns/ns1 CNI_IFNAME=eth10 \
CNI_PATH=`pwd` \
./example < config
For that go CNI framework, those infromation should be passed by the environement
and we can get that from the CmdArgs
.
Actually, we have done the basic CNI program but it does nothing.
A good CNI should make a container network connectivity and assign a valid IP address to the container and we will do that in the foloowing tutorial.
Step 2
Now, we will create a linux bridge for the container and the logical flow looks like
- Read the bridge information from the config.
- Get the bridge name we want to use.
- Create the bridge if it doesn't exist in the system.
Since the frametwork store the config content in the CmdArgs
object as a []byte
form. we should create a structure
to decode those []byte
data.
type SimpleBridge struct {
BridgeName string `json:"bridgeName"`
IP string `json:"ip"`
}
and decode the config content in the CmdAdd
function.
func cmdAdd(args *skel.CmdArgs) error {
sb := SimpleBridge{}
if err := json.Unmarshal(args.StdinData, &sb); err != nil {
return err
}
fmt.Println(sb)
There're many ways for creating the Linuxu Bridge, we can use the system commadn brctl addbr
via the os.Exec
or use the netlink
to create.
We choose the netlink
method here since the os.Exec
is too easy for developer.
First, we should import the netlink
package "github.com/vishvananda/netlink"
and we will use the type netlink.Bridge
to describe the bridge we want.
In the following example, we will do three things.
- Prepare the netlink.Bridge object we want.
- Create the Bridge
- Setup the Linux Bridge.
br := &netlink.Bridge{
LinkAttrs: netlink.LinkAttrs{
Name: sb.BridgeName,
MTU: 1500,
// Let kernel use default txqueuelen; leaving it unset
// means 0, and a zero-length TX queue messes up FIFO
// traffic shapers which use TX queue length as the
// default packet limit
TxQLen: -1,
},
}
err := netlink.LinkAdd(br)
if err != nil && err != syscall.EEXIST {
return err
}
if err := netlink.LinkSetUp(br); err != nil {
return err
}
Now. The CmdAdd
function should look like below.
func cmdAdd(args *skel.CmdArgs) error {
sb := SimpleBridge{}
if err := json.Unmarshal(args.StdinData, &sb); err != nil {
return err
}
fmt.Println(sb)
br := &netlink.Bridge{
LinkAttrs: netlink.LinkAttrs{
Name: sb.BridgeName,
MTU: 1500,
// Let kernel use default txqueuelen; leaving it unset
// means 0, and a zero-length TX queue messes up FIFO
// traffic shapers which use TX queue length as the
// default packet limit
TxQLen: -1,
},
}
err := netlink.LinkAdd(br)
if err != nil && err != syscall.EEXIST {
return err
}
if err := netlink.LinkSetUp(br); err != nil {
return err
}
Use the aforementioned command to call the binary again and you should see the linux bridge test
has been created.
If youu don't have the brctl
command, use the apt-get install bridge-utils to
to install the bridge tools.
Step3
In the next step, we will creat a veth
for connecting the linux
bridge and the taget container
.
The logical flow are
- Get the bridge object from the
Bridge
we created before - Get the namespace of the container
- Create a veth on the container and move the host-end veth to host ns.
- Attach a host-end veth to linux bridge
This step is more complicate then previous steps. since we will handle the network namespace
here.
Fortunately, the CNI
project has provided convenience function to handle the veth and it can cover the (3) action itom above.
First, we use the netlink.LinkByName
method to lookup the netlink
object.
l, err := netlink.LinkByName(sb.BridgeName)
if err != nil {
return fmt.Errorf("could not lookup %q: %v", sb.BridgeName, err)
}
and the we need to make sure that object is netlink.Bridge
, so we do the type casting.
newBr, ok := l.(*netlink.Bridge)
if !ok {
return fmt.Errorf("%q already exists but is not a bridge", sb.BridgeName)
}
Second, since the CmdArgs
already provide the network namespace
path of the container, we can use the method from the ns
package to load the object of the network namespace.
import `"github.com/containernetworking/plugins/pkg/ns"`
netns, err := ns.GetNS(args.Netns)
if err != nil {
return err
}
For each NetNS
object, it implement a function Do
which take a function as its parameter and that function's parameter is the caller's network namespace.
The do
function will switch the network namespace to NetNS
object itself and call the function(parameter) and feed the original network namespace as parameter.
See the following example to learn more about do
function.
var handler = func(hostNS ns.NetNS) error {
hostVeth, containerVeth, err := ip.SetupVeth(args.IfName, 1500, hostNS)
}
if err := netns.Do(handler); err != nil {
return err
}
First , we create a function handler which calls the ip.SetpuVeth
to create a veth pair on caller's network namespace and move one side of veth pair to its third parameter(hostNS
)
When we call the netns.Do(handler)
, it will call the function handler
in netns's
network namepsace and pass the caller's network namespace to the function handler
.
Which will result in that there will be a veth pair between the host's network namespace and netns's
netowkr namespace.
In order to store the information about that veth pair, we can use the current.Interface{}
object to store the data.
First, we need to import the library
import "github.com/containernetworking/cni/pkg/types/current"
and then create a variable represent to host side network interface in the function handler.
hostIface := ¤t.Interface{}
var handler = func(hostNs ns.Netns) error {
hostVeth, _, err := ip.SetupVeth(args.IfName, 1500, hostNS)
if err != nil {
return err
}
hostIface.Name = hostVeth.Name
return nil
}
Now, we can get the interface name of veth pair in the host side by hostIface.Name
and then we will attach that link to the Linux Bridge
we created before.
- Get the link object from the interface name by function call
netlink.LinkByName
- Connect the link to bridge by function call
netlink.LinkSetMaster
hostVeth, err := netlink.LinkByName(hostIface.Name)
if err != nil {
return err
}
if err := netlink.LinkSetMaster(hostVeth, newBr); err != nil {
return err
}
There is one important thing we need to care is the OS thread. since we will switch the netns
to handle the namespace things.
We must make sure the OS won't switch the thread during the namespace
operations.
Use the function runtime.LockOSThread()
in the golang predefined function init()
.
func init() {
// this ensures that main runs only on main thread (thread group leader).
// since namespace ops (unshare, setns) are done for a single thread, we
// must ensure that the goroutine does not jump from OS thread to thread
runtime.LockOSThread()
}```
See the whole example program in https://github.com/hwchiu/CNI_Tutorial_2018/tree/master/tutorial/step3 and you can directly run
the `run.sh` in your linux machine to see the following output.
```shell=
Ready to call the step3 example
{test 192.0.2.1/24}
The CNI has been called, see the following results
The bridge and the veth has been attatch to
bridge name bridge id STP enabled interfaces
test 8000.aa6e12faa09b no vethff65a064
The interface in the netns
eth10 Link encap:Ethernet HWaddr 7e:23:e2:e5:8f:c4
inet6 addr: fe80::7c23:e2ff:fee5:8fc4/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:1 errors:0 dropped:0 overruns:0 frame:0
TX packets:1 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:90 (90.0 B) TX bytes:90 (90.0 B)
lo Link encap:Local Loopback
LOOPBACK MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
We have successfully create a linux bridge and connect to the other network namespace via the veth pair and the interface in that namepsace is eth10
which has been defiend in the config file.
Step4
In this step, we will setup the IP address into the target network namespace.
To make the problem easy, we had set the target IP address in the config and we can get via the sp.IP
type SimpleBridge struct {
BridgeName string `json:"bridgeName"`
IP string `json:"ip"`
}
The function we used to assign the IP address is netlink.AddrAdd
So the workflow is
- Generate a IP object from the config.
- Call the
nelink.AddrAdd
in the target network namespace.
The parameter of netlink.AddrAdd
is netlink.Addr
and see its structure below.
type Addr struct {
*net.IPNet
Label string
Flags int
Scope int
Peer *net.IPNet
Broadcast net.IP
PreferedLft int
ValidLft int
}
We can use the net
package provided by official golang to generate the net.IPNet
type and its a CIDR form (IP address and the Mask).
Since the IP address in our config is a string192.0.2.15/24
,
we use the net.ParseCIDR
to parse the string and return a pointer of net.IPNet
So, modify the previous handler to assign the IP address when we create a veth.
Since the net.IPNet
object get from the net.ParseCIDR
is the subnet
not a real IP
addrees, we should reassign the IP
address to its IP field again.
var handler = func(hostNS ns.NetNS) error {
hostVeth, containerVeth, err := ip.SetupVeth(args.IfName, 1500, hostNS)
if err != nil {
return err
}
hostIface.Name = hostVeth.Name
ipv4Addr, ipv4Net, err := net.ParseCIDR(sb.IP)
if err != nil {
return err
}
link, err := netlink.LinkByName(containerVeth.Name)
if err != nil {
return err
}
ipv4Net.IP = ipv4Addr
addr := &netlink.Addr{IPNet: ipv4Net, Label: ""}
if err = netlink.AddrAdd(link, addr); err != nil {
return err
}
return nil
}
See the whole example program in https://github.com/hwchiu/CNI_Tutorial_2018/tree/master/tutorial/step4 and you can directly run
the run.sh
in your linux machine to see the following output.
Ready to call the step4 example
{test 192.0.2.15/24}
The CNI has been called, see the following results
The bridge and the veth has been attatch to
bridge name bridge id STP enabled interfaces
test 8000.a6f55b2927c0 no vethd611bb3b
The interface in the netns
eth10 Link encap:Ethernet HWaddr aa:a0:96:45:65:c5
inet addr:192.0.2.15 Bcast:192.0.2.255 Mask:255.255.255.0
inet6 addr: fe80::a8a0:96ff:fe45:65c5/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:2 errors:0 dropped:0 overruns:0 frame:0
TX packets:1 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:168 (168.0 B) TX bytes:90 (90.0 B)
lo Link encap:Local Loopback
LOOPBACK MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
And you can see we have already set the IP address to the interface eth10
.
You can use the following command to mamually set the IP address to the linux bridge and use the ping
command to check the network connectiviy between the host and the target network namespace.
sudo ifconfig test 192.0.2.1
sudo ip netns exec ns1 ping 192.0.2.1
Summary
In this tutorial, we have implemented a simple Linux Bridge CNI (only Add function) in golang.
We create the linux bridge and use the veth to connect the linux bridge with the target netowrk namespace. Besides, we also fethc the information we want from the pre-defined config file which means we can more flexible to change the behavior of your own CNI implementation.
To make the problem simple, we don't use any complicated method to acquire a unique address from the config but you can desing you own algorithm to do that. If you want to learn more about the IP related operations, you can go to the host-local to learn more.