CNI Plugin Bridge代码解析
在CNI所以默认提供的Plugin中,bridge
应该算是最简单的插件了,针对IPAM Plugin,最简单的应该是host-local
,这两个插件也是Kubernetes网络kubenet
需要的两个插件。所以这里看一下这两个插件的代码。
所有官方维护的代码,都开源在containernetworking/plugins项目中了。
其中bridge
的代码在plugins/main/bridge
目录,最重要的是cmdAdd
和cmdDel
两个函数,对应CNI SPEC中的ADD和DEL两个主要操作。主要来看一下cmdAdd
的实现,精简(删除一些错误处理)后的代码如下:
func cmdAdd(args *skel.CmdArgs) error {
n, cniVersion, err := loadNetConf(args.StdinData)
if n.IsDefaultGW {
n.IsGW = true
}
br, brInterface, err := setupBridge(n)
if err != nil {
return err
}
netns, err := ns.GetNS(args.Netns)
defer netns.Close()
hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode)
r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
if err != nil {
return err
}
result, err := current.NewResultFromResult(r)
if len(result.IPs) == 0 {
return errors.New("IPAM plugin returned missing IP config")
}
result.Interfaces = []*current.Interface{brInterface, hostInterface, containerInterface}
gwsV4, gwsV6, err := calcGateways(result, n)
if err != nil {
return err
}
if err := netns.Do(func(_ ns.NetNS) error {
contVeth, err := net.InterfaceByName(args.IfName)
for _, ipc := range result.IPs {
if ipc.Version == "6" && (n.HairpinMode || n.PromiscMode) {
if err := disableIPV6DAD(args.IfName); err != nil {
return err
}
break
}
}
if err := ipam.ConfigureIface(args.IfName, result); err != nil {
return err
}
for _, ipc := range result.IPs {
if ipc.Version == "4" {
_ = arping.GratuitousArpOverIface(ipc.Address.IP, *contVeth)
}
}
return nil
}); err != nil {
return err
}
if n.IsGW {
var firstV4Addr net.IP
for _, gws := range []*gwInfo{gwsV4, gwsV6} {
for _, gw := range gws.gws {
if gw.IP.To4() != nil && firstV4Addr == nil {
firstV4Addr = gw.IP
}
err = ensureBridgeAddr(br, gws.family, &gw, n.ForceAddress)
if err != nil {
return fmt.Errorf("failed to set bridge addr: %v", err)
}
}
if gws.gws != nil {
if err = enableIPForward(gws.family); err != nil {
return fmt.Errorf("failed to enable forwarding: %v", err)
}
}
}
}
if n.IPMasq {
chain := utils.FormatChainName(n.Name, args.ContainerID)
comment := utils.FormatComment(n.Name, args.ContainerID)
for _, ipc := range result.IPs {
if err = ip.SetupIPMasq(ip.Network(&ipc.Address), chain, comment); err != nil {
return err
}
}
}
br, err = bridgeByName(n.BrName)
brInterface.Mac = br.Attrs().HardwareAddr.String()
result.DNS = n.DNS
return types.PrintResult(result, cniVersion)
}
主要的就是几大步,首先,调用setupBridge
确保机器上有对应的bridge
,然后再调用setupVeth
在容器对应的namespace下创建好虚拟网络接口。创建完成,就需要通过ipam.ExecAdd(n.IPAM.Type, args.StdinData)
向IPAM插件获取IP地址了。
拿到了IP地址,就会根据IP地址计算对应的路由和网关,然后调用ipam.ConfigureIface(args.IfName, result)
将IP地址设置到对应的虚拟网络接口上,同时,还需要将主机上的bridge
加上网关的IP,并且开启主机的ip_forward
,
最后再调用ip.SetupIPMasq(ip.Network(&ipc.Address), chain, comment)
加上IP转发规则,一切正常,按要求输出结果到stdout
整个Add
操作结束。
而对于IPAM host-local插件,其代码在plugins/ipam/host-local
目录下,主要逻辑同样比较简单,精简后的cmdAdd
函数如下:
func cmdAdd(args *skel.CmdArgs) error {
ipamConf, confVersion, err := allocator.LoadIPAMConfig(args.StdinData, args.Args)
result := ¤t.Result{}
if ipamConf.ResolvConf != "" {
dns, err := parseResolvConf(ipamConf.ResolvConf)
if err != nil {
return err
}
result.DNS = *dns
}
store, err := disk.New(ipamConf.Name, ipamConf.DataDir)
defer store.Close()
allocs := []*allocator.IPAllocator{}
requestedIPs := map[string]net.IP{} //net.IP cannot be a key
for _, ip := range ipamConf.IPArgs {
requestedIPs[ip.String()] = ip
}
for idx, rangeset := range ipamConf.Ranges {
allocator := allocator.NewIPAllocator(&rangeset, store, idx)
// Check to see if there are any custom IPs requested in this range.
var requestedIP net.IP
for k, ip := range requestedIPs {
if rangeset.Contains(ip) {
requestedIP = ip
delete(requestedIPs, k)
break
}
}
ipConf, err := allocator.Get(args.ContainerID, requestedIP)
if err != nil {
// Deallocate all already allocated IPs
for _, alloc := range allocs {
_ = alloc.Release(args.ContainerID)
}
return fmt.Errorf("failed to allocate for range %d: %v", idx, err)
}
allocs = append(allocs, allocator)
result.IPs = append(result.IPs, ipConf)
}
// If an IP was requested that wasn't fulfilled, fail
if len(requestedIPs) != 0 {
for _, alloc := range allocs {
_ = alloc.Release(args.ContainerID)
}
errstr := "failed to allocate all requested IPs:"
for _, ip := range requestedIPs {
errstr = errstr + " " + ip.String()
}
return fmt.Errorf(errstr)
}
result.Routes = ipamConf.Routes
return types.PrintResult(result, confVersion)
}
主要逻辑就是allocator.LoadIPAMConfig
读取IPAM的配置,得到需要分配的IP段,然后针对每个IP段,调用allocator.Get
获取IP地址,再做一些相应的判断等操作,最后输出结果。
其中allocator
需要一个Store实例用来存储已经分配的IP地址信息,默认使用的是plugins/ipam/host-local/backend/disk/backend.go
将信息存到磁盘上,实现很简单,就是每个IP存一个文件,文件内容就是ContainerID
。
有bridge
和host-local
配合,kubenet
就可以正常工作了,但是如果引入更复杂的网络结构,就需要使用更复杂的CNI插件了。