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 | 容器ID | container-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 := ¤t.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 := ¤t.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 插件开发
- 实现基本的 Bridge CNI 插件
- 支持 IP 分配和释放
- 测试插件功能
验证步骤:
# 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. 创建多个网络
./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. 配置容器运行时
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-调试技术与工具 - 调试技术详解
下一步:让我们学习文件系统隔离技术,这是容器存储的基础!