- 云原生
- 网络
- 后端
【免费下载链接】cni
Container Network Interface - networking for Linux containers
导读
debug是 CNI(Container Network Interface)仓库中专门为 CNI 插件开发、调试与排障设计的辅助插件。它可以在插件链(plugin chain)中记录每一个 CNI 操作请求的完整上下文(容器 ID、网络命名空间、接口名、标准输入数据等),并支持在容器网络命名空间内注入自定义排查命令(hooks)。读完本文,你将掌握 debug 插件的全部配置参数、典型链式配置方法,以及如何借助它快速定位插件开发中的问题。
一、debug 插件是什么
根据 plugins/debug/README.md 的定义,该插件的目标非常明确:帮助 CNI 插件开发过程中进行调试(debugging)与排障(troubleshooting)。它本身不负责创建网络,而是作为插件链中的一个"观察者"和"执行器":
- 观察者:通过
cniOutput把每一次 CNI 请求(ADD/DEL/CHECK)的完整参数落盘到文件,供开发者事后分析; - 执行器:通过
addHooks/delHooks/checkHooks在容器网络命名空间内执行自定义 shell 命令,便于开发者在接口创建、删除、检查的关键节点注入排查逻辑。
从源码结构看,debug 插件是一个独立的 Go module(见 plugins/debug/go.mod,模块名为github.com/containernetworking/cni/plugins/debug),通过replace指令指向仓库根目录的 CNI 库本身,依赖github.com/containernetworking/cni v1.1.2与github.com/containernetworking/plugins v1.4.0。
二、从源码看工作原理
debug 插件的全部实现集中在 plugins/debug/main.go,仅约 150 行,核心逻辑非常清晰:
- 配置解析:
parseConf()将标准输入(stdin)中的 JSON 配置反序列化为NetConf结构体。结构体定义了插件的全部配置项(对应 README 中的 Config Reference):
type NetConf struct { types.NetConf CNIOutput string `json:"cniOutput,omitempty"` AddHooks [][]string `json:"addHooks,omitempty"` DelHooks [][]string `json:"delHooks,omitempty"` CheckHooks [][]string `json:"checkHooks,omitempty"` }注册三个生命周期操作:
main()调用skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, ...),即复用 CNI 的 skel 骨架库 完成环境变量解析、版本协商与 stdin 读取,分别处理 ADD、CHECK、DEL 三种 CNI 命令。每个命令两条职责:
- 若配置了
cniOutput,以追加写入(os.O_APPEND)、权限0644的方式打开目标文件,先写入CmdAdd/CmdCheck/CmdDel命令名,再调用outputCmdArgs()落盘完整的请求参数; - 若配置了对应 hooks,调用
executeHooks()进入容器网络命名空间执行命令。
- 若配置了
三、配置项详解(Config Reference)
README 中共定义了 4 个配置项,全部可选:
1.cniOutput(string,可选)
指定一个文件路径,插件会把 CNI 请求输出写入该文件。格式固定为:
命令名(CmdAdd/CmdCheck/CmdDel) ContainerID: <容器ID> Netns: <网络命名空间路径> IfName: <接口名> Args: <CNI_ARGS 扩展参数> Path: <CNI_PATH 插件搜索路径> StdinData: <本次请求的完整 stdin JSON> ----------------------从源码看,outputCmdArgs()使用了fmt.Fprintf按固定模板输出(见 plugins/debug/main.go),其中StdinData是完整的原始 JSON 字符串。ADD 操作会额外输出prevResult(前一插件的结果),因为getResult()会解析RawPrevResult;而 DEL/CHECK 操作返回空结果。这一特性对观察插件链数据流转特别有价值。
2.addHooks(命令数组,可选)
接口 ADD 时,在容器网络命名空间内执行的命令列表。示例中给出的写法为:
"addHooks": [ [ "sh", "-c", "ip link set $CNI_IFNAME promisc on" ] ]注意:虽然 README 描述为 "string array",但实际源码类型是[][]string(命令 + 参数的嵌套数组),第一个元素是命令名(如sh、ip),其余元素是该命令的参数。hooks 内可以直接引用 CNI 环境变量,如$CNI_IFNAME(接口名)、$CNI_CONTAINERID(容器 ID)等。
3.delHooks(命令数组,可选)
接口 DEL(删除)时在容器网络命名空间内执行的命令列表,用途与addHooks对称,常用于清理排查现场。
4.checkHooks(命令数组,可选)
接口 CHECK 时在容器网络命名空间内执行的命令列表。CHECK 操作仅适用于 CNI 规范 v0.4.0 及更高版本,可用来在运行时校验接口状态是否符合预期。
四、完整配置示例与构建安装
4.1 标准插件链配置(conflist)
debug 插件以"链中一环"的形式工作。README 给出了一个非常典型的场景:ptp建网 +debug排障 +portmap端口映射。完整配置如下(需保存为.conflist文件):
{ "cniVersion": "0.3.1", "name": "mynet", "plugins": [ { "type": "ptp", "ipMasq": true, "ipam": { "type": "host-local", "subnet": "172.16.30.0/24", "routes": [ { "dst": "0.0.0.0/0" } ] } }, { "type": "debug", "cniOutput": "/tmp/cni_output.txt", "addHooks": [ [ "sh", "-c", "ip link set $CNI_IFNAME promisc on" ] ] }, { "type": "portmap", "capabilities": {"portMappings": true}, "externalSetMarkChain": "KUBE-MARK-MASQ" } ] }该配置的含义:
- ptp 插件负责创建 veth 对并分配 IP(
172.16.30.0/24子网、默认路由); - debug 插件夹在中间,把 ADD 请求完整记录到
/tmp/cni_output.txt,同时把eth0接口设置为混杂模式(promisc); - portmap 插件最后处理端口映射(依赖 K8s 的
KUBE-MARK-MASQ标记链)。
由于 debug 插件的输入包含前一插件(ptp)的prevResult,落盘日志能完整还原"ptp 分配了哪些 IP/接口"这一信息,方便比对。
4.2 构建与安装
在仓库根目录执行(Go 1.21+):
go build -o debug ./plugins/debug # 或进入插件目录单独构建 cd plugins/debug && go build -o debug .将生成的debug可执行文件放入CNI_PATH指向的目录(如/opt/cni/bin),即可被 cnitool 或容器运行时发现。
4.3 用 cnitool 触发
配合仓库自带的 cnitool(CNI 命令行工具)即可手动复现完整插件链。cnitool 通过CNI_PATH查找插件,通过NETCONFPATH(默认/etc/cni/net.d)读取配置,*.conflist文件优先级高于*.conf:
sudo ip netns add testing sudo CNI_PATH=/opt/cni/bin cnitool add mynet /var/run/netns/testing cat /tmp/cni_output.txt # 查看 ADD 请求记录 sudo CNI_PATH=/opt/cni/bin cnitool check mynet /var/run/netns/testing sudo CNI_PATH=/opt/cni/bin cnitool del mynet /var/run/netns/testing sudo ip netns del testing其中cnitool add的底层调用是libcni的AddNetworkList()(见 cnitool/cmd/add.go),会按顺序执行 conflist 中的每个插件并把前序结果作为prevResult传入,这正是 debug 插件能记录完整链式上下文的原因。
五、Sample CNI Output 深度解读
README 给出了两次真实请求的落盘样例(下为 ADD 部分):
CmdAdd ContainerID: cnitool-20c433bb2b1d6ede56d6 Netns: /var/run/netns/cnitest IfName: eth0 Args: Path: /opt/cni/bin StdinData: {"cniOutput":"/tmp/cni_output.txt","cniVersion":"0.3.1","name":"test","prevResult":{"cniVersion":"0.3.1","interfaces":[{"name":"veth92e295cc","mac":"56:22:7f:b7:5b:75"},{"name":"eth0","mac":"46:b3:f3:77:bf:21","sandbox":"/var/run/netns/cnitest"}],"ips":[{"version":"4","interface":1,"address":"10.1.1.2/24","gateway":"10.1.1.1"}],"dns":{"nameservers":["10.64.255.25","8.8.8.8"]}},"type":"none"} ----------------------逐字段解读:
| 字段 | 含义 | 排查价值 |
|---|---|---|
CmdAdd | 本次触发的 CNI 命令 | 判断生命周期事件是否按预期发生 |
ContainerID | 容器 ID(来自CNI_CONTAINERID) | 关联具体容器实例 |
Netns | 容器网络命名空间路径(来自CNI_NETNS) | 确认命名空间是否正确、是否存在 |
IfName | 容器内接口名(来自CNI_IFNAME) | 确认接口命名是否符合预期 |
Args | CNI_ARGS扩展参数(K8s 中常含IgnoreUnknown=1等) | 排查参数传递问题 |
Path | 插件搜索路径(CNI_PATH) | 确认是否加载了正确的插件二进制 |
StdinData | 插件收到的完整 JSON 配置 | 核心排查项,包含prevResult全量数据 |
StdinData中的prevResult是插件链调试的关键:它展示了 ptp 插件返回的接口列表(veth92e295cc与eth0)、MAC 地址、IP 分配(10.1.1.2/24,网关10.1.1.1)以及 DNS 配置。如果后续插件行为异常,对照该数据即可判断是"上游结果错误"还是"下游处理错误"。
DEL 记录则相对精简——StdinData中不再携带prevResult,只有插件自身配置与type字段,符合 CNI 规范中 DEL 请求的语义。
六、Hooks 的执行细节与注意事项
6.1 执行位置:容器网络命名空间
executeHooks()的实现(见 plugins/debug/main.go)揭示了 hooks 的运行机制:
- 通过
ns.GetNS(netnsName)打开CNI_NETNS指向的命名空间; - 调用
netns.Do(...)将当前线程切换进该命名空间(底层由pkg/ns封装,Linux 实现见 pkg/ns/ns_linux.go); - 在命名空间内依次
exec.Command执行每个 hook,命令与参数即[][]string配置中的元素。
这意味着 hooks 里执行的ip link、ip addr等命令看到的都是容器内的网络环境,可以直接操作$CNI_IFNAME对应的接口——这是该插件能"在接口创建现场注入命令"的根本原因。
6.2 两个重要局限(务必注意)
- 不捕获命令失败:README 明确指出 "just execute it and does not catch command failure"。从源码看,
executeHooks()即使遇到exec.Command报错,也只是把输出和错误打印到 stderr,不向 CNI 运行时返回错误,ADD/DEL 依然成功。因此 hooks 适合"旁路排查",不能用于强校验; - 命名空间不可用时静默跳过:若
ns.GetNS失败(例如 DEL 时命名空间已被删除),函数直接返回,不会中断流程。这在调试"容器已销毁后的清理路径"时需要注意。
6.3 典型 hook 用法
"addHooks": [ [ "sh", "-c", "ip link set $CNI_IFNAME promisc on" ], [ "ip", "addr", "show", "$CNI_IFNAME" ] ]第一条将接口置为混杂模式(对抓包排查有意义),第二条在命名空间内打印接口地址信息。也可以写日志文件:
"addHooks": [ [ "sh", "-c", "ip addr show $CNI_IFNAME > /tmp/netns_eth0.txt 2>&1" ] ]七、适用场景与最佳实践
综合文档与源码,debug 插件最适合以下场景:
- CNI 插件链开发调试:在自研插件前后插入 debug 插件,通过
cniOutput观察上下游数据(尤其prevResult)是否符合预期; - 接口配置现场取证:用
addHooks/delHooks在命名空间内抓取接口状态、路由表,作为问题定位的现场快照; - 运行时状态校验辅助:配合
cnitool check(仅 v0.4.0+ 规范)观察 CHECK 路径的请求内容。
实践中建议:
- 将
cniOutput指向独立文件(如/tmp/cni_output.txt),借助追加写模式连续记录多次操作,便于对比 ADD/DEL 的差异; - hooks 命令保持"只读取证 + 轻量修改",因为命令失败不会回滚也不影响 CNI 结果;
- 排查完毕后及时从 conflist 中移除 debug 插件,避免生产环境产生额外 IO 与副作用。
八、总结
debug 插件用极简的设计(一个结构体、三个回调、一个输出函数)解决了 CNI 开发中最头痛的"黑盒"问题:请求参数看不见、命名空间内状态摸不着。通过cniOutput记录请求全貌、通过三类 hooks 注入现场命令,配合 cnitool 手动触发插件链,开发者可以低成本地复现、观察和定位插件链中的绝大多数问题。其完整实现仅一个文件 plugins/debug/main.go,非常适合作为学习 CNI 插件骨架(skel)、版本协商(version.All)与网络命名空间切换机制的入门范本。
- 云原生
- 网络
- 后端
【免费下载链接】cni
Container Network Interface - networking for Linux containers
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考