CNI是Cloud Native Computing Foundation项目,为Linux容器提供配置网络接口的标准和以该标 准扩展插件提供基础函数库,当前在0.8+的版本上也已经支持windows插件。CNI仅关注两件事情:容器 网络资源分配、容器网络资源释放。网络资源主要包含两个层面:网络设备资源、IP资源。
CNI社区主要包含两个项目:
CNI标准规范插件是一个可执行主体,执行过程需要符合以下四个方面规范,但是并未限定具体实现。
输入输出的细节可以查看CNI社区定义:https://github.com/containernetworking/cni/blob/master/SPEC.md,不同版本会有一些差别。
社区已经提供了一些通用的插件,收录在plugins项目中。主要目录结构如下:
plugins
├── ipam
│ ├── dhcp
│ ├── host-local
│ └── static
├── main
│ ├── bridge
│ ├── host-device
│ ├── ipvlan
│ ├── loopback
│ ├── macvlan
│ ├── ptp
│ ├── vlan
│ └── windows
└── meta
├── bandwidth
├── firewall
├── flannel
├── portmap
├── sbr
└── tuning
为方便社区协作,CNI针对容器网络定义提供了golang的接口(0.6.0版本):https://github.com/containernetworking/cni/blob/v0.6.0/libcni/api.go#L51
type CNI interface {
AddNetworkList(net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
DelNetworkList(net *NetworkConfigList, rt *RuntimeConf) error
AddNetwork(net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
DelNetwork(net *NetworkConfig, rt *RuntimeConf) error
}
接口只定义了四个方法:增加网络,删除网络,增加网络列表,删除网络列表。其中NetworkConfig/NetworkConfigList 是CNI标准定义的JSON输入,RuntimeConf是CNI定义的每次运行时需要输入的环境变量。
如果我们只需要扩展单个CNI插件,一般已经不再需要直接实现该接口。针对JSON配置解析,环境变量解析, CNI动作判定,社区都已经完成封装:https://github.com/containernetworking/cni/blob/v0.6.0/pkg/skel/skel.go#L221
// PluginMain is the core "main" for a plugin which includes automatic error handling.
//
// The caller must also specify what CNI spec versions the plugin supports.
//
// When an error occurs in either cmdAdd or cmdDel, PluginMain will print the error
// as JSON to stdout and call os.Exit(1).
//
// To have more control over error handling, use PluginMainWithError() instead.
func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) {
if e := PluginMainWithError(cmdAdd, cmdDel, versionInfo); e != nil {
if err := e.Print(); err != nil {
log.Print("Error writing error JSON to stdout: ", err)
}
os.Exit(1)
}
}
我们只需要对PluginMain注入我们自己实现增加网络(cmdAdd),删除网络(cmdDel)的函数实现即可。 cmdAdd与cmdDel都需要接受参数CmdArgs,这个是封装了CNI的JSON配置和环境变量两部分输入。
type CmdArgs struct {
ContainerID string //需要设置网络的容器ID,来自环境变量CNI_CONTAINERID
Netns string //容器的网络命名空间,来自环境变量CNI_NETNS
IfName string //容器内网卡的名称,来自环境变量CNI_IFNAME
Args string //CNI参数,来自环境变量CNI_ARGS
Path string //CNI二进制工具目录,来自环境变量CNI_PATH
StdinData []byte //CNI插件Json配置,需要自行解析内容
}
stdinData来自CNI插件Json配置,不同的插件该部分都有所差异,所以在CmdArgs中使用字符数组存储, 各CNI插件根据自行定义的具体结构体再进行反序列化。
我们通过分析社区的ptp插件看下如何实现一个CNI插件。https://github.com/containernetworking/plugins/tree/v0.6.0/plugins/main/ptp
PTP主要是通过veth pair + 路由控制来实现容器网络:
PTP的cmdAdd相关步骤
func cmdAdd(args *skel.CmdArgs) error {
//step1:通过CmdArgs.StdinData解析自定义的CNI json配置
conf := NetConf{}
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
return fmt.Errorf("failed to load netconf: %v", err)
}
//step2:借助CNI库封装,调用JSON文件中指定的ipam工具,申请IP地址
r, err := ipam.ExecAdd(conf.IPAM.Type, args.StdinData)
if err != nil {
return err
}
//step3:格式化ipam工具的返回结果,避免不同版本之间的结果不兼容的问题
result, err := current.NewResultFromResult(r)
if err != nil {
return err
}
if len(result.IPs) == 0 {
return errors.New("IPAM plugin returned missing IP config")
}
if err := ip.EnableForward(result.IPs); err != nil {
return fmt.Errorf("Could not enable IP forwarding: %v", err)
}
netns, err := ns.GetNS(args.Netns)
if err != nil {
return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
}
defer netns.Close()
//step4:创建veth pair,一端设置到容器中,并配置申请到的IP地址
hostInterface, containerInterface, err := setupContainerVeth(netns, args.IfName, conf.MTU, result)
if err != nil {
return err
}
//step5,在主机节点设置容器IP的路由规则
if err = setupHostVeth(hostInterface.Name, result); err != nil {
return err
}
//step6,如果开启了ipMasq特性,设置SNAT规则
if conf.IPMasq {
chain := utils.FormatChainName(conf.Name, args.ContainerID)
comment := utils.FormatComment(conf.Name, args.ContainerID)
for _, ipc := range result.IPs {
if err = ip.SetupIPMasq(&ipc.Address, chain, comment); err != nil {
return err
}
}
}
result.DNS = conf.DNS
result.Interfaces = []*current.Interface{hostInterface, containerInterface}
//step7,将结果格式化,并打印到输出流
return types.PrintResult(result, conf.CNIVersion)
}
PTP的cmdDel
func cmdDel(args *skel.CmdArgs) error {
//step1:通过CmdArgs.StdinData解析自定义的CNI json配置
conf := NetConf{}
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
return fmt.Errorf("failed to load netconf: %v", err)
}
//step2:调用json配置中指定的ipam工具,释放IP地址资源
if err := ipam.ExecDel(conf.IPAM.Type, args.StdinData); err != nil {
return err
}
if args.Netns == "" {
return nil
}
//step3:切换到容器网络命名空间中,清理网卡
var ipn *net.IPNet
err := ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
var err error
ipn, err = ip.DelLinkByNameAddr(args.IfName, netlink.FAMILY_V4)
if err != nil && err == ip.ErrLinkNotFound {
return nil
}
return err
})
if err != nil {
return err
}
//如果开启了IPMasq特性,则删掉在add阶段增加SNAT规则
if ipn != nil && conf.IPMasq {
chain := utils.FormatChainName(conf.Name, args.ContainerID)
comment := utils.FormatComment(conf.Name, args.ContainerID)
err = ip.TeardownIPMasq(ipn, chain, comment)
}
return err
}
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。