HiHuo
首页
博客
手册
工具
首页
博客
手册
工具
  • 手撸容器系统

    • 完整手撸容器技术文档系列
    • 01-容器本质与基础概念
    • 02-Namespace隔离机制
    • 03-CGroup资源控制
    • 04-Capabilities与安全机制
    • 05-容器网络原理
    • 06-网络模式与实现
    • 07-CNI插件开发
    • 08-RootFS与文件系统隔离
    • 09-OverlayFS镜像分层
    • 10-命令行手撸容器
    • 11-Go实现最小容器
    • 12-Go实现完整容器
    • 13-容器生命周期管理
    • 14-调试技术与工具
    • 15-OCI规范与标准化
    • 16-进阶场景与优化
    • 常见问题与故障排查
    • 参考资料与延伸阅读

07-CNI插件开发

学习目标

  • 深入理解 CNI 规范和接口设计
  • 掌握 CNI 插件的开发方法
  • 实现一个简单的 Bridge CNI 插件
  • 了解 IPAM (IP地址管理) 的实现
  • 掌握 CNI 插件的测试和调试

前置知识

  • 容器网络基础原理
  • Go 语言编程
  • Linux 网络配置
  • JSON 数据处理

一、CNI 规范详解

1.1 CNI 概述

CNI (Container Network Interface) 是容器网络接口规范:

graph TD
    A[容器运行时] --> B[CNI 插件]
    B --> C[网络配置]
    C --> D[网络接口]
    C --> E[IP 地址]
    C --> F[路由规则]
    
    G[CNI 规范] --> H[ADD 操作]
    G --> I[DEL 操作]
    G --> J[CHECK 操作]
    G --> K[VERSION 操作]

1.2 CNI 核心概念

概念说明示例
插件可执行文件,实现网络配置/opt/cni/bin/bridge
配置JSON 格式的网络配置bridge.conf
网络网络名称空间default
接口网络接口名称eth0

1.3 CNI 操作类型

graph LR
    A[CNI 操作] --> B[ADD - 添加网络]
    A --> C[DEL - 删除网络]
    A --> D[CHECK - 检查网络]
    A --> E[VERSION - 版本信息]
    
    B --> B1[创建网络接口]
    B --> B2[分配 IP 地址]
    B --> B3[配置路由]
    
    C --> C1[删除网络接口]
    C --> C2[释放 IP 地址]
    C --> C3[清理路由]
    
    D --> D1[检查接口状态]
    D --> D2[验证 IP 配置]
    D --> D3[测试连通性]

️ 二、CNI 插件开发基础

2.1 CNI 插件接口

CNI 插件通过标准输入/输出进行通信:

// CNI 插件输入格式
type CNIInput struct {
    CNIVersion string                 `json:"cniVersion"`
    Name       string                 `json:"name"`
    Type       string                 `json:"type"`
    Args       map[string]interface{} `json:"args,omitempty"`
    IPAM       IPAMConfig            `json:"ipam,omitempty"`
    DNS        DNSConfig             `json:"dns,omitempty"`
}

// CNI 插件输出格式
type CNIOutput struct {
    CNIVersion string    `json:"cniVersion"`
    Interfaces []Interface `json:"interfaces,omitempty"`
    IPs        []IPConfig `json:"ips,omitempty"`
    Routes     []Route    `json:"routes,omitempty"`
    DNS        DNSConfig  `json:"dns,omitempty"`
}

2.2 CNI 插件环境变量

变量说明示例
CNI_COMMAND操作类型ADD, DEL, CHECK, VERSION
CNI_CONTAINERID容器IDcontainer-123
CNI_NETNS网络命名空间路径/proc/12345/ns/net
CNI_IFNAME接口名称eth0
CNI_ARGS额外参数K8S_POD_NAME=pod-123
CNI_PATH插件搜索路径/opt/cni/bin

2.3 CNI 插件开发框架

package main

import (
    "encoding/json"
    "fmt"
    "os"
    "github.com/containernetworking/cni/pkg/skel"
    "github.com/containernetworking/cni/pkg/version"
)

func main() {
    skel.PluginMain(cmdAdd, cmdDel, version.All)
}

func cmdAdd(args *skel.CmdArgs) error {
    // 解析配置
    var config CNIInput
    if err := json.Unmarshal(args.StdinData, &config); err != nil {
        return fmt.Errorf("解析配置失败: %v", err)
    }
    
    // 实现网络配置逻辑
    // ...
    
    // 返回结果
    result := CNIOutput{
        CNIVersion: config.CNIVersion,
        Interfaces: []Interface{...},
        IPs:        []IPConfig{...},
        Routes:     []Route{...},
        DNS:        config.DNS,
    }
    
    return json.NewEncoder(os.Stdout).Encode(result)
}

func cmdDel(args *skel.CmdArgs) error {
    // 实现网络清理逻辑
    // ...
    return nil
}

三、Bridge CNI 插件实现

3.1 Bridge 插件设计

graph TD
    A[Bridge CNI 插件] --> B[解析配置]
    B --> C[创建 Bridge]
    C --> D[创建 veth pair]
    D --> E[连接 veth 到 Bridge]
    E --> F[配置容器网络]
    F --> G[分配 IP 地址]
    G --> H[配置路由]
    H --> I[返回结果]

3.2 完整实现代码

package main

import (
    "encoding/json"
    "fmt"
    "net"
    "os"
    "os/exec"
    "path/filepath"
    "strings"
    
    "github.com/containernetworking/cni/pkg/skel"
    "github.com/containernetworking/cni/pkg/types"
    "github.com/containernetworking/cni/pkg/types/current"
    "github.com/containernetworking/cni/pkg/version"
    "github.com/containernetworking/plugins/pkg/ip"
    "github.com/containernetworking/plugins/pkg/ipam"
    "github.com/containernetworking/plugins/pkg/utils"
    "github.com/vishvananda/netlink"
)

type BridgeNetConf struct {
    types.NetConf
    BrName       string `json:"bridge"`
    IsGW         bool   `json:"isGateway"`
    IsDefaultGW  bool   `json:"isDefaultGateway"`
    ForceAddress bool   `json:"forceAddress"`
    IPMasq       bool   `json:"ipMasq"`
    MTU          int    `json:"mtu"`
    IPAM         struct {
        Type string `json:"type"`
    } `json:"ipam"`
}

func loadNetConf(bytes []byte) (*BridgeNetConf, error) {
    n := &BridgeNetConf{
        BrName:      "cni0",
        IsGW:        true,
        IsDefaultGW: true,
        IPMasq:      true,
        MTU:         1500,
    }
    
    if err := json.Unmarshal(bytes, n); err != nil {
        return nil, fmt.Errorf("解析配置失败: %v", err)
    }
    
    if n.CNIVersion == "" {
        n.CNIVersion = "0.3.1"
    }
    
    return n, nil
}

func setupBridge(netconf *BridgeNetConf) (*netlink.Bridge, error) {
    // 创建或获取 Bridge
    br, err := netlink.LinkByName(netconf.BrName)
    if err != nil {
        if err.Error() == "Link not found" {
            br = &netlink.Bridge{
                LinkAttrs: netlink.LinkAttrs{
                    Name: netconf.BrName,
                },
            }
            if err := netlink.LinkAdd(br); err != nil {
                return nil, fmt.Errorf("创建 Bridge 失败: %v", err)
            }
        } else {
            return nil, fmt.Errorf("获取 Bridge 失败: %v", err)
        }
    }
    
    // 启动 Bridge
    if err := netlink.LinkSetUp(br); err != nil {
        return nil, fmt.Errorf("启动 Bridge 失败: %v", err)
    }
    
    return br.(*netlink.Bridge), nil
}

func setupVeth(netconf *BridgeNetConf, br *netlink.Bridge, ifName string, mtu int) (*netlink.Veth, error) {
    // 生成 veth 名称
    hostVethName := fmt.Sprintf("veth%s", ifName[:min(len(ifName), 4)])
    
    // 创建 veth pair
    veth := &netlink.Veth{
        LinkAttrs: netlink.LinkAttrs{
            Name: hostVethName,
            MTU:  mtu,
        },
        PeerName: ifName,
    }
    
    if err := netlink.LinkAdd(veth); err != nil {
        return nil, fmt.Errorf("创建 veth pair 失败: %v", err)
    }
    
    // 将 host 端连接到 Bridge
    if err := netlink.LinkSetMaster(veth, br); err != nil {
        return nil, fmt.Errorf("连接 veth 到 Bridge 失败: %v", err)
    }
    
    // 启动 host 端
    if err := netlink.LinkSetUp(veth); err != nil {
        return nil, fmt.Errorf("启动 veth 失败: %v", err)
    }
    
    return veth, nil
}

func setupContainerVeth(veth *netlink.Veth, ifName string, netnsPath string) error {
    // 获取容器端 veth
    peerLink, err := netlink.LinkByName(veth.PeerName)
    if err != nil {
        return fmt.Errorf("获取容器端 veth 失败: %v", err)
    }
    
    // 将容器端移到目标 namespace
    if err := netlink.LinkSetNsPid(peerLink, 0); err != nil {
        return fmt.Errorf("移动 veth 到容器 namespace 失败: %v", err)
    }
    
    // 在容器 namespace 中配置网络
    return configureContainerInterface(netnsPath, ifName, veth.PeerName)
}

func configureContainerInterface(netnsPath, ifName, peerName string) error {
    // 在容器 namespace 中执行网络配置
    cmd := exec.Command("ip", "netns", "exec", netnsPath, "ip", "link", "set", peerName, "name", ifName)
    if err := cmd.Run(); err != nil {
        return fmt.Errorf("重命名容器接口失败: %v", err)
    }
    
    cmd = exec.Command("ip", "netns", "exec", netnsPath, "ip", "link", "set", ifName, "up")
    if err := cmd.Run(); err != nil {
        return fmt.Errorf("启动容器接口失败: %v", err)
    }
    
    return nil
}

func setupIPMasq(ipn *net.IPNet) error {
    // 配置 iptables MASQUERADE 规则
    cmd := exec.Command("iptables", "-t", "nat", "-A", "POSTROUTING", 
        "-s", ipn.String(), "-j", "MASQUERADE")
    return cmd.Run()
}

func cmdAdd(args *skel.CmdArgs) error {
    // 解析配置
    netconf, err := loadNetConf(args.StdinData)
    if err != nil {
        return err
    }
    
    // 创建 Bridge
    br, err := setupBridge(netconf)
    if err != nil {
        return err
    }
    
    // 创建 veth pair
    veth, err := setupVeth(netconf, br, args.IfName, netconf.MTU)
    if err != nil {
        return err
    }
    
    // 配置容器网络
    if err := setupContainerVeth(veth, args.IfName, args.Netns); err != nil {
        return err
    }
    
    // 分配 IP 地址
    result := &current.Result{
        CNIVersion: netconf.CNIVersion,
        Interfaces: []*current.Interface{
            {
                Name:    args.IfName,
                Mac:     veth.Attrs().HardwareAddr.String(),
                Sandbox: args.Netns,
            },
        },
    }
    
    // 使用 IPAM 分配 IP
    if netconf.IPAM.Type != "" {
        ipamResult, err := ipam.ExecAdd(netconf.IPAM.Type, args.StdinData)
        if err != nil {
            return fmt.Errorf("IPAM 分配失败: %v", err)
        }
        
        // 解析 IPAM 结果
        ipamResult, err = current.NewResultFromResult(ipamResult)
        if err != nil {
            return fmt.Errorf("解析 IPAM 结果失败: %v", err)
        }
        
        result.IPs = ipamResult.(*current.Result).IPs
        result.Routes = ipamResult.(*current.Result).Routes
        result.DNS = ipamResult.(*current.Result).DNS
    }
    
    // 配置 IP Masquerade
    if netconf.IPMasq && len(result.IPs) > 0 {
        if err := setupIPMasq(&result.IPs[0].Address.IPNet); err != nil {
            return fmt.Errorf("配置 IP Masquerade 失败: %v", err)
        }
    }
    
    // 返回结果
    return types.PrintResult(result, netconf.CNIVersion)
}

func cmdDel(args *skel.CmdArgs) error {
    // 解析配置
    netconf, err := loadNetConf(args.StdinData)
    if err != nil {
        return err
    }
    
    // 清理 IPAM
    if netconf.IPAM.Type != "" {
        if err := ipam.ExecDel(netconf.IPAM.Type, args.StdinData); err != nil {
            return fmt.Errorf("IPAM 清理失败: %v", err)
        }
    }
    
    // 清理 veth pair
    if args.IfName != "" {
        if err := cleanupVeth(args.IfName); err != nil {
            return fmt.Errorf("清理 veth 失败: %v", err)
        }
    }
    
    return nil
}

func cleanupVeth(ifName string) error {
    // 查找并删除 veth pair
    links, err := netlink.LinkList()
    if err != nil {
        return err
    }
    
    for _, link := range links {
        if veth, ok := link.(*netlink.Veth); ok {
            if veth.PeerName == ifName {
                return netlink.LinkDel(veth)
            }
        }
    }
    
    return nil
}

func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

func main() {
    skel.PluginMain(cmdAdd, cmdDel, version.All)
}

四、IPAM 插件实现

4.1 IPAM 插件设计

graph TD
    A[IPAM 插件] --> B[分配 IP 地址]
    A --> C[释放 IP 地址]
    A --> D[检查 IP 状态]
    
    B --> B1[从地址池分配]
    B --> B2[检查冲突]
    B --> B3[更新状态]
    
    C --> C1[标记为可用]
    C --> C2[清理状态]
    
    D --> D1[检查 IP 是否已分配]
    D --> D2[验证 IP 有效性]

4.2 简单 IPAM 实现

package main

import (
    "encoding/json"
    "fmt"
    "net"
    "os"
    "path/filepath"
    "sync"
    
    "github.com/containernetworking/cni/pkg/skel"
    "github.com/containernetworking/cni/pkg/types"
    "github.com/containernetworking/cni/pkg/types/current"
    "github.com/containernetworking/cni/pkg/version"
)

type IPAMConfig struct {
    Type       string `json:"type"`
    Subnet     string `json:"subnet"`
    RangeStart string `json:"rangeStart"`
    RangeEnd   string `json:"rangeEnd"`
    Gateway    string `json:"gateway"`
    Routes     []Route `json:"routes"`
}

type Route struct {
    Dst string `json:"dst"`
    GW  string `json:"gw"`
}

type IPAMState struct {
    AllocatedIPs map[string]bool `json:"allocatedIPs"`
    Subnet       string          `json:"subnet"`
    Gateway      string          `json:"gateway"`
    Routes       []Route         `json:"routes"`
}

type SimpleIPAM struct {
    stateFile string
    state     *IPAMState
    mutex     sync.Mutex
}

func NewSimpleIPAM(stateFile string) *SimpleIPAM {
    return &SimpleIPAM{
        stateFile: stateFile,
        state:     &IPAMState{AllocatedIPs: make(map[string]bool)},
    }
}

func (ipam *SimpleIPAM) loadState() error {
    ipam.mutex.Lock()
    defer ipam.mutex.Unlock()
    
    data, err := os.ReadFile(ipam.stateFile)
    if err != nil {
        if os.IsNotExist(err) {
            return nil // 首次运行,使用默认状态
        }
        return err
    }
    
    return json.Unmarshal(data, ipam.state)
}

func (ipam *SimpleIPAM) saveState() error {
    ipam.mutex.Lock()
    defer ipam.mutex.Unlock()
    
    data, err := json.MarshalIndent(ipam.state, "", "  ")
    if err != nil {
        return err
    }
    
    // 确保目录存在
    if err := os.MkdirAll(filepath.Dir(ipam.stateFile), 0755); err != nil {
        return err
    }
    
    return os.WriteFile(ipam.stateFile, data, 0644)
}

func (ipam *SimpleIPAM) allocateIP(subnet, rangeStart, rangeEnd string) (string, error) {
    if err := ipam.loadState(); err != nil {
        return "", err
    }
    
    // 解析子网
    _, ipNet, err := net.ParseCIDR(subnet)
    if err != nil {
        return "", fmt.Errorf("解析子网失败: %v", err)
    }
    
    // 生成 IP 地址范围
    startIP := net.ParseIP(rangeStart)
    endIP := net.ParseIP(rangeEnd)
    
    if startIP == nil || endIP == nil {
        return "", fmt.Errorf("无效的 IP 地址范围")
    }
    
    // 遍历 IP 地址范围,找到第一个未分配的
    for ip := startIP; ipNet.Contains(ip); ip = nextIP(ip) {
        ipStr := ip.String()
        
        if !ipam.state.AllocatedIPs[ipStr] {
            ipam.state.AllocatedIPs[ipStr] = true
            ipam.state.Subnet = subnet
            if err := ipam.saveState(); err != nil {
                return "", err
            }
            return ipStr, nil
        }
        
        // 检查是否超出范围
        if ip.Equal(endIP) {
            break
        }
    }
    
    return "", fmt.Errorf("没有可用的 IP 地址")
}

func (ipam *SimpleIPAM) releaseIP(ip string) error {
    if err := ipam.loadState(); err != nil {
        return err
    }
    
    delete(ipam.state.AllocatedIPs, ip)
    return ipam.saveState()
}

func nextIP(ip net.IP) net.IP {
    next := make(net.IP, len(ip))
    copy(next, ip)
    
    for i := len(next) - 1; i >= 0; i-- {
        next[i]++
        if next[i] != 0 {
            break
        }
    }
    
    return next
}

func cmdAdd(args *skel.CmdArgs) error {
    // 解析配置
    var config IPAMConfig
    if err := json.Unmarshal(args.StdinData, &config); err != nil {
        return fmt.Errorf("解析配置失败: %v", err)
    }
    
    // 创建 IPAM 实例
    ipam := NewSimpleIPAM("/var/lib/cni/ipam/simple-ipam.json")
    
    // 分配 IP 地址
    ip, err := ipam.allocateIP(config.Subnet, config.RangeStart, config.RangeEnd)
    if err != nil {
        return err
    }
    
    // 解析网关
    gateway := net.ParseIP(config.Gateway)
    if gateway == nil {
        return fmt.Errorf("无效的网关地址")
    }
    
    // 创建结果
    result := &current.Result{
        CNIVersion: "0.3.1",
        IPs: []*current.IPConfig{
            {
                Version: "4",
                Address: net.IPNet{
                    IP:   net.ParseIP(ip),
                    Mask: net.CIDRMask(24, 32), // 假设 /24 子网
                },
                Gateway: gateway,
            },
        },
        Routes: []*types.Route{},
        DNS:    types.DNS{},
    }
    
    // 添加路由
    for _, route := range config.Routes {
        result.Routes = append(result.Routes, &types.Route{
            Dst: net.IPNet{
                IP:   net.ParseIP(route.Dst),
                Mask: net.CIDRMask(32, 32),
            },
            GW: net.ParseIP(route.GW),
        })
    }
    
    return types.PrintResult(result, "0.3.1")
}

func cmdDel(args *skel.CmdArgs) error {
    // 解析配置
    var config IPAMConfig
    if err := json.Unmarshal(args.StdinData, &config); err != nil {
        return fmt.Errorf("解析配置失败: %v", err)
    }
    
    // 创建 IPAM 实例
    ipam := NewSimpleIPAM("/var/lib/cni/ipam/simple-ipam.json")
    
    // 释放 IP 地址
    if err := ipam.releaseIP(args.ContainerID); err != nil {
        return err
    }
    
    return nil
}

func main() {
    skel.PluginMain(cmdAdd, cmdDel, version.All)
}

五、CNI 插件测试

5.1 测试环境搭建

#!/bin/bash
# 搭建 CNI 插件测试环境

echo "=== 搭建 CNI 测试环境 ==="

# 1. 创建测试目录
mkdir -p /tmp/cni-test/{bin,conf,netns}

# 2. 编译插件
go build -o /tmp/cni-test/bin/bridge ./bridge-plugin
go build -o /tmp/cni-test/bin/simple-ipam ./ipam-plugin

# 3. 创建网络配置
cat > /tmp/cni-test/conf/bridge.conf << 'EOF'
{
  "cniVersion": "0.3.1",
  "name": "bridge-test",
  "type": "bridge",
  "bridge": "cni0",
  "isGateway": true,
  "isDefaultGateway": true,
  "ipMasq": true,
  "ipam": {
    "type": "simple-ipam",
    "subnet": "192.168.100.0/24",
    "rangeStart": "192.168.100.10",
    "rangeEnd": "192.168.100.20",
    "gateway": "192.168.100.1"
  }
}
EOF

# 4. 创建测试网络命名空间
ip netns add test-ns

echo "=== 测试环境搭建完成 ==="
echo "插件路径: /tmp/cni-test/bin"
echo "配置文件: /tmp/cni-test/conf/bridge.conf"
echo "测试命名空间: test-ns"

5.2 插件功能测试

#!/bin/bash
# 测试 CNI 插件功能

echo "=== 测试 CNI 插件 ==="

# 1. 测试 ADD 操作
echo "--- 测试 ADD 操作 ---"
CNI_COMMAND=ADD \
CNI_CONTAINERID=test-container \
CNI_NETNS=/proc/$(ip netns pids test-ns)/ns/net \
CNI_IFNAME=eth0 \
CNI_PATH=/tmp/cni-test/bin \
/tmp/cni-test/bin/bridge < /tmp/cni-test/conf/bridge.conf

# 2. 检查网络配置
echo "--- 检查网络配置 ---"
ip netns exec test-ns ip addr show
ip netns exec test-ns ip route show

# 3. 测试连通性
echo "--- 测试连通性 ---"
ip netns exec test-ns ping -c 3 192.168.100.1

# 4. 测试 DEL 操作
echo "--- 测试 DEL 操作 ---"
CNI_COMMAND=DEL \
CNI_CONTAINERID=test-container \
CNI_NETNS=/proc/$(ip netns pids test-ns)/ns/net \
CNI_IFNAME=eth0 \
CNI_PATH=/tmp/cni-test/bin \
/tmp/cni-test/bin/bridge < /tmp/cni-test/conf/bridge.conf

# 5. 清理
echo "--- 清理测试环境 ---"
ip netns del test-ns

5.3 自动化测试脚本

package main

import (
    "encoding/json"
    "fmt"
    "os"
    "os/exec"
    "path/filepath"
    "testing"
)

func TestCNIPlugin(t *testing.T) {
    // 创建测试环境
    testDir := "/tmp/cni-test"
    if err := os.MkdirAll(testDir, 0755); err != nil {
        t.Fatalf("创建测试目录失败: %v", err)
    }
    
    // 编译插件
    if err := exec.Command("go", "build", "-o", 
        filepath.Join(testDir, "bridge"), "./bridge-plugin").Run(); err != nil {
        t.Fatalf("编译插件失败: %v", err)
    }
    
    // 创建测试配置
    config := map[string]interface{}{
        "cniVersion": "0.3.1",
        "name":       "bridge-test",
        "type":       "bridge",
        "bridge":     "cni0",
        "isGateway":  true,
        "ipMasq":     true,
        "ipam": map[string]interface{}{
            "type":        "host-local",
            "subnet":      "192.168.100.0/24",
            "rangeStart":  "192.168.100.10",
            "rangeEnd":    "192.168.100.20",
            "gateway":     "192.168.100.1",
        },
    }
    
    configData, err := json.Marshal(config)
    if err != nil {
        t.Fatalf("序列化配置失败: %v", err)
    }
    
    // 创建测试网络命名空间
    if err := exec.Command("ip", "netns", "add", "test-ns").Run(); err != nil {
        t.Fatalf("创建网络命名空间失败: %v", err)
    }
    defer exec.Command("ip", "netns", "del", "test-ns").Run()
    
    // 测试 ADD 操作
    cmd := exec.Command(filepath.Join(testDir, "bridge"))
    cmd.Env = []string{
        "CNI_COMMAND=ADD",
        "CNI_CONTAINERID=test-container",
        "CNI_NETNS=/proc/$(ip netns pids test-ns)/ns/net",
        "CNI_IFNAME=eth0",
        "CNI_PATH=" + testDir,
    }
    cmd.Stdin = strings.NewReader(string(configData))
    
    output, err := cmd.Output()
    if err != nil {
        t.Fatalf("ADD 操作失败: %v", err)
    }
    
    // 验证结果
    var result map[string]interface{}
    if err := json.Unmarshal(output, &result); err != nil {
        t.Fatalf("解析结果失败: %v", err)
    }
    
    // 检查网络配置
    if err := exec.Command("ip", "netns", "exec", "test-ns", 
        "ip", "addr", "show").Run(); err != nil {
        t.Fatalf("检查网络配置失败: %v", err)
    }
    
    // 测试 DEL 操作
    cmd = exec.Command(filepath.Join(testDir, "bridge"))
    cmd.Env = []string{
        "CNI_COMMAND=DEL",
        "CNI_CONTAINERID=test-container",
        "CNI_NETNS=/proc/$(ip netns pids test-ns)/ns/net",
        "CNI_IFNAME=eth0",
        "CNI_PATH=" + testDir,
    }
    cmd.Stdin = strings.NewReader(string(configData))
    
    if err := cmd.Run(); err != nil {
        t.Fatalf("DEL 操作失败: %v", err)
    }
    
    fmt.Println("CNI 插件测试通过")
}

六、验证检查清单

基础理解

  • [ ] 理解 CNI 规范和接口设计
  • [ ] 掌握 CNI 插件的开发方法
  • [ ] 了解 IPAM 的实现原理
  • [ ] 理解 CNI 插件的测试方法

实践能力

  • [ ] 能够开发简单的 CNI 插件
  • [ ] 能够实现 IPAM 功能
  • [ ] 能够进行插件测试和调试
  • [ ] 能够集成到容器运行时

高级技能

  • [ ] 掌握复杂网络插件的开发
  • [ ] 能够进行性能优化
  • [ ] 能够处理错误和异常
  • [ ] 理解 CNI 生态和最佳实践

实战实现

完整的 CNI 插件实现

根据实际开发经验,以下是完整的 CNI 插件实现代码:

1. 基础 CNI 插件结构

package main

import (
    "encoding/json"
    "fmt"
    "net"
    "os"
    "path/filepath"
    "runtime"
    
    "github.com/containernetworking/cni/pkg/skel"
    "github.com/containernetworking/cni/pkg/types"
    "github.com/containernetworking/cni/pkg/version"
    "github.com/vishvananda/netlink"
    "golang.org/x/sys/unix"
)

type PluginConf struct {
    types.NetConf
    Bridge string `json:"bridge"`
    Subnet string `json:"subnet"`
    Gateway string `json:"gateway"`
    IPAM   *IPAMConfig `json:"ipam,omitempty"`
}

type IPAMConfig struct {
    Type string `json:"type"`
    Subnet string `json:"subnet"`
    Gateway string `json:"gateway"`
    Routes []types.Route `json:"routes,omitempty"`
}

type IPAM struct {
    Subnet    *net.IPNet
    Gateway   net.IP
    Allocated map[string]bool
    file      string
}

func main() {
    skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, "CNI plugin for bridge networking")
}

2. ADD 命令实现

func cmdAdd(args *skel.CmdArgs) error {
    // 解析配置
    conf, err := parseConfig(args.StdinData)
    if err != nil {
        return fmt.Errorf("failed to parse config: %v", err)
    }
    
    // 确保 bridge 存在
    bridge, err := ensureBridge(conf.Bridge, conf.Subnet)
    if err != nil {
        return fmt.Errorf("failed to create bridge: %v", err)
    }
    
    // 创建 veth 对
    hostVeth, contVeth, err := createVethPair(args.ContainerID)
    if err != nil {
        return fmt.Errorf("failed to create veth pair: %v", err)
    }
    
    // 分配 IP
    ip, err := allocateIP(conf.IPAM)
    if err != nil {
        _ = netlink.LinkDel(hostVeth)
        return fmt.Errorf("failed to allocate IP: %v", err)
    }
    
    // 配置网络
    if err := configureNetwork(hostVeth, contVeth, ip, bridge); err != nil {
        _ = netlink.LinkDel(hostVeth)
        return fmt.Errorf("failed to configure network: %v", err)
    }
    
    // 构建结果
    result := &types.Result{
        CNIVersion: conf.CNIVersion,
        IPs: []*types.IPConfig{
            {
                Version: "4",
                Address: *ip,
                Gateway: net.ParseIP(conf.Gateway),
            },
        },
    }
    
    return types.PrintResult(result, conf.CNIVersion)
}

3. 网络配置实现

func ensureBridge(name, subnet string) (*netlink.Bridge, error) {
    // 检查 bridge 是否已存在
    link, err := netlink.LinkByName(name)
    if err == nil {
        if bridge, ok := link.(*netlink.Bridge); ok {
            return bridge, nil
        }
        return nil, fmt.Errorf("interface %s exists but is not a bridge", name)
    }
    
    // 创建新的 bridge
    bridge := &netlink.Bridge{
        LinkAttrs: netlink.LinkAttrs{
            Name: name,
        },
    }
    
    if err := netlink.LinkAdd(bridge); err != nil {
        return nil, fmt.Errorf("failed to create bridge: %v", err)
    }
    
    // 设置 IP 地址
    ip, ipNet, err := net.ParseCIDR(subnet)
    if err != nil {
        return nil, fmt.Errorf("invalid subnet: %v", err)
    }
    
    addr := &netlink.Addr{
        IPNet: &net.IPNet{
            IP:   ip,
            Mask: ipNet.Mask,
        },
    }
    
    if err := netlink.AddrAdd(bridge, addr); err != nil {
        return nil, fmt.Errorf("failed to set bridge IP: %v", err)
    }
    
    // 启动 bridge
    if err := netlink.LinkSetUp(bridge); err != nil {
        return nil, fmt.Errorf("failed to bring up bridge: %v", err)
    }
    
    return bridge, nil
}

func createVethPair(containerID string) (*netlink.Veth, *netlink.Veth, error) {
    hostVethName := fmt.Sprintf("vethh-%s", containerID[:8])
    contVethName := fmt.Sprintf("vethc-%s", containerID[:8])
    
    veth := &netlink.Veth{
        LinkAttrs: netlink.LinkAttrs{
            Name: hostVethName,
        },
        PeerName: contVethName,
    }
    
    if err := netlink.LinkAdd(veth); err != nil {
        return nil, nil, fmt.Errorf("failed to create veth pair: %v", err)
    }
    
    // 获取对端接口
    peer, err := netlink.LinkByName(contVethName)
    if err != nil {
        _ = netlink.LinkDel(veth)
        return nil, nil, fmt.Errorf("failed to get peer interface: %v", err)
    }
    
    contVeth, ok := peer.(*netlink.Veth)
    if !ok {
        _ = netlink.LinkDel(veth)
        return nil, nil, fmt.Errorf("peer is not a veth interface")
    }
    
    return veth, contVeth, nil
}

4. IPAM 实现

func allocateIP(ipamConf *IPAMConfig) (*net.IPNet, error) {
    if ipamConf == nil {
        return nil, fmt.Errorf("IPAM configuration is required")
    }
    
    ipam, err := NewIPAM(ipamConf)
    if err != nil {
        return nil, fmt.Errorf("failed to create IPAM: %v", err)
    }
    
    ip, err := ipam.Allocate()
    if err != nil {
        return nil, fmt.Errorf("failed to allocate IP: %v", err)
    }
    
    return &net.IPNet{
        IP:   ip,
        Mask: ipam.Subnet.Mask,
    }, nil
}

func NewIPAM(conf *IPAMConfig) (*IPAM, error) {
    _, ipNet, err := net.ParseCIDR(conf.Subnet)
    if err != nil {
        return nil, err
    }
    
    gateway := net.ParseIP(conf.Gateway)
    if gateway == nil {
        return nil, fmt.Errorf("invalid gateway: %s", conf.Gateway)
    }
    
    return &IPAM{
        Subnet:    ipNet,
        Gateway:   gateway,
        Allocated: make(map[string]bool),
        file:      "/var/lib/cni/networks/bridge/ipam.json",
    }, nil
}

func (i *IPAM) Allocate() (net.IP, error) {
    // 加载已分配的 IP
    i.load()
    
    start := binaryInc(i.Gateway)
    for ip := start; i.Subnet.Contains(ip); ip = binaryInc(ip) {
        if !i.Allocated[ip.String()] {
            i.Allocated[ip.String()] = true
            i.save()
            return ip, nil
        }
    }
    
    return nil, fmt.Errorf("no available IP in subnet %s", i.Subnet.String())
}

func (i *IPAM) load() error {
    data, err := os.ReadFile(i.file)
    if err != nil {
        return nil // 文件不存在,使用空映射
    }
    
    return json.Unmarshal(data, &i.Allocated)
}

func (i *IPAM) save() error {
    if err := os.MkdirAll(filepath.Dir(i.file), 0755); err != nil {
        return err
    }
    
    data, err := json.Marshal(i.Allocated)
    if err != nil {
        return err
    }
    
    return os.WriteFile(i.file, data, 0644)
}

func binaryInc(ip net.IP) net.IP {
    res := make(net.IP, len(ip))
    copy(res, ip)
    for j := len(res) - 1; j >= 0; j-- {
        res[j]++
        if res[j] != 0 {
            break
        }
    }
    return res
}

5. DEL 和 CHECK 命令实现

func cmdDel(args *skel.CmdArgs) error {
    // 解析配置
    conf, err := parseConfig(args.StdinData)
    if err != nil {
        return fmt.Errorf("failed to parse config: %v", err)
    }
    
    // 查找并删除 veth 接口
    hostVethName := fmt.Sprintf("vethh-%s", args.ContainerID[:8])
    if link, err := netlink.LinkByName(hostVethName); err == nil {
        _ = netlink.LinkDel(link)
    }
    
    // 释放 IP
    if conf.IPAM != nil {
        releaseIP(conf.IPAM, args.ContainerID)
    }
    
    return nil
}

func cmdCheck(args *skel.CmdArgs) error {
    // 检查网络接口是否存在
    hostVethName := fmt.Sprintf("vethh-%s", args.ContainerID[:8])
    if _, err := netlink.LinkByName(hostVethName); err != nil {
        return fmt.Errorf("veth interface %s not found", hostVethName)
    }
    
    // 检查 bridge 是否存在
    conf, err := parseConfig(args.StdinData)
    if err != nil {
        return err
    }
    
    if _, err := netlink.LinkByName(conf.Bridge); err != nil {
        return fmt.Errorf("bridge %s not found", conf.Bridge)
    }
    
    return nil
}

高级 CNI 插件特性

1. 多网络支持

type MultiNetworkPlugin struct {
    networks map[string]*NetworkConfig
    mu       sync.RWMutex
}

type NetworkConfig struct {
    Name     string
    Bridge   string
    Subnet   string
    Gateway  string
    IPAM     *IPAMConfig
    Routes   []types.Route
}

func (p *MultiNetworkPlugin) AddNetwork(networkName string, config *NetworkConfig) error {
    p.mu.Lock()
    defer p.mu.Unlock()
    
    p.networks[networkName] = config
    return nil
}

2. 网络策略支持

type NetworkPolicy struct {
    IngressRules []IngressRule
    EgressRules  []EgressRule
}

type IngressRule struct {
    From   []NetworkPeer
    Ports  []PortRule
    Action string // ALLOW, DENY
}

type EgressRule struct {
    To    []NetworkPeer
    Ports []PortRule
    Action string
}

func (p *MultiNetworkPlugin) ApplyPolicy(containerID string, policy *NetworkPolicy) error {
    // 实现网络策略应用逻辑
    return nil
}

3. 监控和指标

type NetworkMetrics struct {
    BytesReceived   uint64
    BytesSent       uint64
    PacketsReceived uint64
    PacketsSent     uint64
    Errors          uint64
}

func (p *MultiNetworkPlugin) GetMetrics(containerID string) (*NetworkMetrics, error) {
    // 从 /proc/net/dev 读取网络统计信息
    return nil, nil
}

实战练习

练习 1:基础 CNI 插件开发

  1. 实现基本的 Bridge CNI 插件
  2. 支持 IP 分配和释放
  3. 测试插件功能

验证步骤:

# 1. 编译插件
go build -o bridge-cni main.go

# 2. 配置 CNI
mkdir -p /etc/cni/net.d
cat > /etc/cni/net.d/10-bridge.conf << EOF
{
  "cniVersion": "0.4.0",
  "name": "bridge",
  "type": "bridge-cni",
  "bridge": "cni0",
  "subnet": "10.22.0.0/24",
  "gateway": "10.22.0.1",
  "ipam": {
    "type": "host-local",
    "subnet": "10.22.0.0/24",
    "gateway": "10.22.0.1"
  }
}
EOF

# 3. 测试插件
echo '{"cniVersion": "0.4.0", "name": "bridge", "type": "bridge-cni"}' | \
  CNI_COMMAND=ADD CNI_CONTAINERID=test123 CNI_NETNS=/proc/1/ns/net CNI_IFNAME=eth0 \
  ./bridge-cni

练习 2:多网络插件

  1. 实现多网络支持
  2. 添加网络策略功能
  3. 实现监控指标

验证步骤:

# 1. 创建多个网络
./bridge-cni add-network --name=frontend --subnet=10.22.1.0/24
./bridge-cni add-network --name=backend --subnet=10.22.2.0/24

# 2. 应用网络策略
./bridge-cni apply-policy --container=app1 --policy=policy.json

# 3. 查看网络指标
./bridge-cni get-metrics --container=app1

练习 3:插件集成

  1. 与容器运行时集成
  2. 支持动态配置
  3. 实现故障恢复

验证步骤:

# 1. 配置容器运行时
export CNI_PATH=/opt/cni/bin
export CNI_CONF_PATH=/etc/cni/net.d

# 2. 运行容器
docker run --network=bridge-cni nginx

# 3. 验证网络配置
docker exec container_name ip addr show

性能优化

1. 网络性能优化

  • 使用 SR-IOV 技术
  • 启用网络加速功能
  • 优化数据包处理

2. 资源管理

  • 限制网络资源使用
  • 监控网络状态
  • 自动清理资源

3. 高可用性

  • 实现故障转移
  • 支持热插拔
  • 提供健康检查

相关链接

  • 05-容器网络原理 - 网络基础原理
  • 06-网络模式与实现 - 网络模式详解
  • 14-调试技术与工具 - 调试技术详解

下一步:让我们学习文件系统隔离技术,这是容器存储的基础!

Prev
06-网络模式与实现
Next
08-RootFS与文件系统隔离