
Pod 已经被调度到节点，不代表容器可以立刻启动。kubelet 还要通过 CRI 请求容器运行时创建 Pod sandbox；运行时为 sandbox 准备网络命名空间，再执行 CNI 插件。只有这一步成功，Pod 才有可供 init container 和业务容器共享的网络环境。

排查网络问题时，经常会看到 `CNI ADD failed`、`failed to destroy network` 或 `NetworkPluginNotReady`。这些信息对应的不是同一个阶段：有的在创建单个 Pod 的网络，有的在清理 Pod，有的只检查插件能否继续接收新请求。

本文以 [CNI 1.1.0 规范](https://github.com/containernetworking/cni/blob/df52e9460d3a819920b63e28c4d5d6f12bed6758/SPEC.md)为基准，说明 `ADD`、`DEL`、`CHECK`、`STATUS`、`GC` 和 `VERSION` 分别处理什么，以及它们怎样进入 Kubernetes 的 Pod 生命周期。

这里需要先区分两套独立的版本号：`1.1.0` 是运行时与插件之间的 **CNI 协议规范版本**；`v1.3.1` 是 `containernetworking/cni` 代码库的 **软件发布版本**，对应 `libcni`、`pkg/skel` 等 Go 包。代码库发布到 `v1.3.1`，不代表协议规范也变成了 `1.3.1`。截至本文编写时，官方 `SPEC.md` 标注的规范版本仍是 `1.1.0`，并明确说明它与代码库及插件的发布版本相互独立。

<!--more-->

## 先区分 kubelet、容器运行时和 CNI 插件

CNI 规定的是「运行时如何执行网络插件」，不是 kubelet 直接调用某个 CNI Daemon 的 API。这个调用链可以简化为：

```text
kubelet
  -> CRI RunPodSandbox / StopPodSandbox / Status
    -> containerd、CRI-O 等容器运行时
      -> 执行 CNI 插件二进制
        -> 配置网卡、IP、路由、规则或外部网络状态
```

CNI 插件通常是一个可执行文件。运行时通过环境变量传入 `CNI_COMMAND`、容器 ID、网络命名空间和接口名，通过标准输入传入 JSON 配置；插件通过退出码和标准输出返回结果。它不是 CSI 或 Device Plugin 那种先向 kubelet 注册、再通过长期运行的 gRPC 服务接收请求的接口。

从 Kubernetes 1.24 开始，kubelet 不再负责 `cni-bin-dir`、`network-plugin` 等 CNI 管理参数，CNI 配置和插件加载属于容器运行时的职责。Kubernetes 当前要求插件至少兼容 CNI v0.4.0，并推荐兼容 v1.0.0，详见 [Kubernetes Network Plugins 文档](https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/)。这也意味着：规范里存在某个操作，不等于集群当前的运行时和插件一定会调用、实现它。

## 六个操作分别在哪个阶段生效

| 操作 | 作用对象 | 常见触发阶段 | 主要作用 |
|---|---|---|---|
| `VERSION` | 插件二进制 | 加载配置、选择协议版本时 | 查询插件支持的 CNI 规范版本，不修改 Pod 网络 |
| `STATUS` | 整个插件或网络配置 | 运行时初始化、CRI 状态检查或后台健康检查时 | 判断插件是否还能处理新的 `ADD` 请求 |
| `ADD` | 一个网络 attachment | `RunPodSandbox` 创建网络时 | 把 sandbox 加入网络，创建或调整接口、IP、路由等 |
| `CHECK` | 已存在的 attachment | `ADD` 成功后到 `DEL` 前，由运行时按需触发 | 对照上次 `ADD` 结果检查网络状态是否仍符合预期 |
| `DEL` | 一个网络 attachment | `StopPodSandbox`、删除 sandbox，或 `ADD` 失败后的清理阶段 | 撤销 `ADD` 创建的网络资源 |
| `GC` | 一个网络配置下的全部 attachment | 运行时维护、重启恢复或周期性垃圾回收时 | 根据有效 attachment 集合清理遗漏的陈旧资源 |

这里的 attachment 不是 Kubernetes API 对象。CNI 用 `(CNI_CONTAINERID, CNI_IFNAME)` 唯一标识一次网络连接。一个 Pod sandbox 的默认网卡通常形成一个 attachment；附加网络也可能形成其他 attachment。

```mermaid
sequenceDiagram
    participant K as kubelet
    participant R as CRI runtime
    participant P as CNI plugin chain

    K->>R: RunPodSandbox
    R->>R: 创建 sandbox 和网络命名空间
    R->>P: VERSION（按需协商）
    R->>P: ADD（正序执行插件链）
    P-->>R: 接口、IP、路由和 DNS 结果
    R-->>K: sandbox 网络配置完成
    Note over K,R: 随后才创建 init container 和业务容器
    opt attachment 存续期间，运行时支持并主动触发
        R->>P: CHECK
        P-->>R: 当前状态是否符合 ADD 结果
    end
    opt 运行时健康检查
        R->>P: STATUS
        P-->>R: 是否可继续处理 ADD
    end
    opt 运行时维护或恢复
        R->>P: GC（携带仍有效的 attachment）
        P-->>R: 清理陈旧资源
    end
    K->>R: StopPodSandbox
    R->>P: DEL（逆序执行插件链）
    R->>R: 删除网络命名空间
```

这张图表达的是规范能力和常见生命周期，不是要求每个运行时在每个 Pod 上依次调用六个操作。对单个 Pod 来说，稳定存在的主路径是创建时的 `ADD` 和销毁时的 `DEL`；其他操作是否出现，要看 CNI 版本、容器运行时实现和配置。

## ADD：Pod sandbox 创建网络时生效

`ADD` 是最常见的 CNI 操作。容器运行时先创建网络命名空间，再为目标接口执行 CNI 插件。插件可以：

- 在 sandbox 中创建接口，例如 veth 的容器侧；
- 调整已经存在的接口；
- 分配 IP，写入路由和 DNS 结果；
- 在主机或外部系统中建立转发、隧道、端口映射等状态。

调用时的关键输入包括：

| 输入 | 含义 |
|---|---|
| `CNI_COMMAND=ADD` | 本次执行的操作 |
| `CNI_CONTAINERID` | 运行时分配的 sandbox 或容器标识 |
| `CNI_NETNS` | 目标网络命名空间路径 |
| `CNI_IFNAME` | 要在目标命名空间中创建或修改的接口名，通常是 `eth0` |
| stdin JSON | `cniVersion`、网络名、插件配置和运行时能力参数 |

成功的 `ADD` 会返回 CNI Result，其中可以包含接口、IP 地址、路由和 DNS。运行时必须持久保存插件链的最终结果，因为后续 `CHECK` 和 `DEL` 需要使用它。

在 Kubernetes 中，这一步处于 Pod sandbox 建立阶段。Kubernetes 的 [`PodReadyToStartContainers` 条件](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-readiness-to-start-containers)会反映 sandbox 和网络是否已经准备完成。网络配置成功后，kubelet 才继续拉取镜像、创建 init container 和业务容器。因此：

- `ADD` 失败通常会让 Pod 停留在 `ContainerCreating`，并产生 `FailedCreatePodSandBox` 事件；
- 重启 Pod 内的单个业务容器通常不会再次调用 `ADD`，因为它仍使用原来的 Pod sandbox 网络命名空间；
- sandbox 被删除并重建时，才会重新建立网络并调用新的 `ADD`；
- `hostNetwork: true` 的 Pod 使用宿主机网络命名空间，通常不经过默认 Pod 网络的 CNI `ADD`。

[containerd 的 CRI 架构说明](https://github.com/containerd/containerd/blob/f6132dbe1f482cbe0aebc4bd3d8d7a184fb4a2aa/docs/cri/architecture.md)也把这段顺序写成：CRI 创建 Pod 网络命名空间，用 CNI 配置网络，然后再创建和启动 sandbox 及业务容器。不同运行时的内部对象顺序可以不同，但 CRI 返回成功前必须完成 sandbox 所需的网络配置。

## DEL：sandbox 清理时生效

`DEL` 撤销对应 `ADD` 创建或修改的状态，例如：

- 释放 IPAM 分配；
- 删除主机侧接口；
- 撤销端口映射、防火墙或流量控制规则；
- 删除外部网络系统中的 endpoint。

正常路径上，kubelet 请求运行时停止 Pod sandbox，运行时先执行网络清理，再删除网络命名空间。本文检查的 containerd 提交可以在 [`StopPodSandbox`](https://github.com/containerd/containerd/blob/f6132dbe1f482cbe0aebc4bd3d8d7a184fb4a2aa/internal/cri/server/sandbox_stop.go)路径中看到这段调用。

`DEL` 有三个容易忽略的约束：

1. 即使 `ADD` 返回失败，运行时也必须继续尝试对应的删除操作，因为前面的插件可能已经分配了 IP 或创建了主机资源。
2. `CNI_NETNS` 对 `DEL` 是可选参数。namespace 可能已经消失，插件仍应尽可能释放 IPAM、主机规则和外部资源。
3. `DEL` 必须可以重复执行。目标资源已经不存在时，插件应把它当成清理完成，而不是制造一个无法收敛的错误。

所以 `DEL` 更接近「幂等、尽力而为的回收接口」，不是对 namespace 内接口做一次简单的 `ip link del`。

## CHECK：attachment 存续期间的状态校验

`CHECK` 从 CNI v0.4.0 引入，用于检查一次已经成功建立的 attachment。运行时要把上次 `ADD` 的最终 Result 作为 `prevResult` 传给插件，插件据此检查自己负责的状态，例如：

- 预期接口、IP 和路由是否仍存在；
- 防火墙、流量控制和 IP 预留是否仍有效；
- 插件依赖的外部 Daemon 是否可用；
- 是否已经知道该 sandbox 整体不可达。

它只允许在 `ADD` 成功之后、`DEL` 之前调用，网络配置中的 `disableCheck: true` 可以显式禁止检查。

`CHECK` 不是 Kubernetes 的 Pod readinessProbe，也不是规范化的端到端连通性探测。它检查的是插件所知道的配置状态；业务进程能否访问某个 Service、DNS 是否返回预期结果，仍需其他探针和观测手段。

还要注意「支持接口」和「实际调用」的差别。本文检查的 containerd 提交依赖 `go-cni` v1.1.14；这个库暴露了 `Check` 方法，但 containerd CRI 的常规 sandbox 创建、状态查询和停止路径没有自动调用它。因此不能看到 CNI 版本大于 v0.4.0，就假设 kubelet 会周期性地替每个 Pod 执行 `CHECK`。

## STATUS：检查插件能否继续服务 ADD

`STATUS` 是 CNI v1.1.0 的插件级健康检查。它不携带某个 Pod 的 `CNI_CONTAINERID`、`CNI_NETNS` 或 `CNI_IFNAME`，检查的是插件是否已经准备好处理新的 `ADD`。

如果插件依赖外部服务，或者 IP、硬件队列等有限资源已经耗尽，可以通过 `STATUS` 返回错误。规范定义了两个相关错误码：

- `50`：插件不可用，不能处理新的 `ADD`；
- `51`：插件不可用，并且现有容器的网络也可能受限。

但 `STATUS` 是信息性的。插件不能依赖运行时一定先调用它；即使它返回失败，规范也不保证运行时会阻止后续所有 `ADD` 或 `DEL`。

在 Kubernetes 这一层，kubelet 通过 CRI `Status` 获取 `NetworkReady`。容器运行时怎样得出这个状态属于运行时实现。在本文检查的 containerd 提交中，CRI `Status` 会调用 `go-cni.Status()`；后者对 CNI v1.1.0 及以上的配置执行插件链 `STATUS`，较老版本则跳过。因此 `NetworkPluginNotReady` 可能来自配置尚未加载，也可能来自插件的 `STATUS` 失败，排查时要继续看运行时错误消息，不能只靠状态名判断根因。

## GC：清理 DEL 遗漏的陈旧资源

`GC` 也是 CNI v1.1.0 引入的操作。它面向整个网络配置，而不是某一个 Pod。运行时把仍然有效的 `(containerID, ifname)` 集合传给插件，插件可以据此清理集合之外的 IPAM 预留、防火墙规则等陈旧资源。

它主要处理节点崩溃、运行时重启或异常清理留下的孤儿状态，但不能替代 `DEL`。原因是 `GC` 调用时可能已经没有原来的 namespace 和完整 attachment 上下文，有些只有 `DEL` 才能安全撤销的资源已经无法识别。

规范还要求运行时在执行 `GC` 时排斥 `ADD` 和 `DEL`：开始 GC 前要等正在执行的增删结束，GC 完成前也不能发起新的增删。这避免插件把刚创建、但尚未写入有效集合的 attachment 当成垃圾回收。

是否主动执行 `GC` 取决于容器运行时。网络配置也可以通过 `disableGC: true` 禁止它，例如同一个网络配置由多个运行时共享、单个运行时无法给出完整有效集合时。排查 IP 泄漏时，不能因为插件声明支持 CNI 1.1.0，就假设运行时一定已经执行了 GC。

## VERSION：协议协商阶段生效

`VERSION` 查询插件支持哪些 CNI 规范版本。它不需要 container ID、网络命名空间或接口名，也不会创建或检查 Pod 网络。

网络配置通过 `cniVersion` 和 `cniVersions` 声明可用版本，插件通过 `VERSION` 返回 `supportedVersions`。运行时可以利用这些信息选择双方和配置都支持的版本。CNI 规范版本与 `containernetworking/cni` Go 库或 CNI plugins 发布版本彼此独立，看到软件包版本号时不能直接把它当成规范版本。

在日志里看到 `VERSION` 失败，通常意味着插件二进制、网络配置和运行时之间无法就协议版本达成一致；它发生在 attachment 真正建立之前，不表示某个已经运行的 Pod 突然断网。

## 插件链：ADD 正序，DEL 逆序

CNI 配置可以是一条插件链。下面的示例先用 `bridge` 创建接口和分配 IP，再应用 `tuning`，最后增加 `portmap` 规则：

```json
{
  "cniVersion": "1.1.0",
  "name": "pod-network",
  "plugins": [
    {
      "type": "bridge",
      "bridge": "cni0",
      "ipam": {
        "type": "host-local",
        "subnet": "10.244.0.0/16"
      }
    },
    {
      "type": "tuning"
    },
    {
      "type": "portmap",
      "capabilities": {
        "portMappings": true
      }
    }
  ]
}
```

执行顺序是：

```text
ADD:    bridge -> tuning -> portmap
CHECK:  bridge -> tuning -> portmap
DEL:    portmap -> tuning -> bridge
GC:     bridge -> tuning -> portmap
STATUS: bridge -> tuning -> portmap
```

`ADD` 中，后一个插件通过 `prevResult` 接收前一个插件的结果并继续修改。运行时保存最后一个插件返回的 Result。`CHECK` 和 `DEL` 则都接收这份最终结果，其中 `DEL` 逆序执行，先撤销后加的规则，再删除底层接口和 IPAM 状态。

这里还有两种「插件调用插件」的情况，不要和运行时的插件链混淆：

- `bridge` 等主插件可以把 IP 分配委托给 `host-local`、`dhcp` 等 IPAM 插件；
- Multus 等 meta plugin 可以根据附加网络定义继续调用其他 CNI 插件。

被委托的插件仍要参与相应的 `DEL`、`CHECK`、`GC` 或 `STATUS`。如果委托的 `ADD` 失败，上层插件应尝试执行对应的 `DEL`，避免留下半完成资源。

## 从 Kube-OVN 和 Cilium 看实现差异

规范只定义调用契约。一个具体插件注册了哪些回调、CNI 配置选择了哪个规范版本、回调内部是直接操作网络还是请求常驻 Agent，必须回到对应版本的源码确认。

下面对比两个固定源码快照：

- Kube-OVN v1.17.0 开发分支提交 [`a39b9533`](https://github.com/jimyag/kube-ovn/tree/a39b953338f43b4168d9b3368ad312bbe1fc96fb)；
- Cilium 1.19.0-dev 提交 [`2c59ba7d`](https://github.com/cilium/cilium/tree/2c59ba7dfaa5065e3b71814b97be7d217e07cda3)。

| 操作 | Kube-OVN 快照 | Cilium 快照 |
|---|---|---|
| `VERSION` | 使用 CNI 库的 `version.All` 返回库支持版本 | 显式声明支持 v0.1.0 到 v1.1.0 |
| `ADD` | 解析参数后，通过 Unix socket 请求 kube-ovn daemon 的 `/api/v1/add` | 请求 cilium-agent 获取配置和 IPAM，再创建 veth/netkit、配置接口并创建 endpoint |
| `DEL` | 通过 Unix socket 请求 daemon 的 `/api/v1/del` | 删除 Agent endpoint，先释放委托 IPAM，再尽力删除 namespace 内接口 |
| `CHECK` | 没有注册实际处理回调 | 检查 Agent endpoint 健康状态，并对照 `prevResult` 检查接口和 IP 地址 |
| `STATUS` | 没有注册实际处理回调 | 检查 cilium-agent healthz；使用委托 IPAM 时继续调用下层插件的 `STATUS` |
| `GC` | 没有注册实际处理回调 | 没有注册实际处理回调 |

### Kube-OVN：CNI 二进制把 ADD 和 DEL 转给 daemon

Kube-OVN 的 [`cmd/cni/cni.go`](https://github.com/jimyag/kube-ovn/blob/a39b953338f43b4168d9b3368ad312bbe1fc96fb/cmd/cni/cni.go)只在 `skel.CNIFuncs` 中注册了 `Add: cmdAdd` 和 `Del: cmdDel`。

`cmdAdd` 自己不直接完成整套 OVS/OVN 配置。它从 CNI 参数中取出 Pod、namespace、container ID、netns 和 ifname，再通过 `server_socket` 请求节点上的 kube-ovn daemon。daemon 只暴露 [`/api/v1/add` 和 `/api/v1/del`](https://github.com/jimyag/kube-ovn/blob/a39b953338f43b4168d9b3368ad312bbe1fc96fb/pkg/daemon/server.go) 两个 CNI 内部接口，实际完成地址等待、网卡、OVS 端口及相关节点配置。CNI 二进制最后把 daemon 返回的接口、IP、路由、MTU 和 DNS 转成标准 CNI Result。

这个快照随镜像安装的 [`01-kube-ovn.conflist`](https://github.com/jimyag/kube-ovn/blob/a39b953338f43b4168d9b3368ad312bbe1fc96fb/dist/images/01-kube-ovn.conflist)使用 `cniVersion: 0.3.1`，插件链是 `kube-ovn -> portmap`。因此创建时先执行 kube-ovn 的 `ADD`，再添加端口映射；删除时顺序相反。这个配置版本早于引入 `CHECK` 的 v0.4.0，也早于 `STATUS`、`GC` 所在的 v1.1.0，运行时不会对这份默认配置发起这些新操作。

源码依赖的 [CNI skeleton 对空回调直接返回成功](https://github.com/containernetworking/cni/blob/v1.3.1/pkg/skel/skel.go#L190-L202)，因此如果另行把配置版本提高到 v1.1.0，`CHECK`、`STATUS` 或 `GC` 可能表现为「调用成功但没有执行检查或回收」。所以判断能力时不能只看 `VERSION` 输出或退出码，还要检查插件注册的 `CNIFuncs` 和实际配置版本。

### Cilium：CNI 二进制与 cilium-agent 共同完成生命周期

Cilium 的 [`PluginMain`](https://github.com/cilium/cilium/blob/2c59ba7dfaa5065e3b71814b97be7d217e07cda3/plugins/cilium-cni/cmd/cmd.go#L100-L122)注册了 `Add`、`Del`、`Check` 和 `Status`，并显式声明支持到 CNI v1.1.0。

在非 chaining 模式下，`ADD` 会先连接 cilium-agent，获取节点配置并请求 IPAM，然后由 CNI 进程创建 veth 或 netkit、把容器侧接口移入 netns，最后把 endpoint 交给 Agent。数据面的后续维护不局限在这次短暂的 CNI 进程里，常驻的 cilium-agent 还会继续管理 endpoint 和 eBPF 状态。

它的 `CHECK` 会执行两层校验：先通过 Agent 查询 `(containerID, ifname)` 对应 endpoint 的健康状态，再进入目标 netns，对照 `prevResult` 检查接口及预期 IP。`STATUS` 则查询 cilium-agent 的 healthz；如果配置使用 `host-local` 等委托 IPAM，还会把 `STATUS` 继续传给 IPAM 插件。

不过，Cilium 这个快照由 Agent 生成的[默认独立模式配置](https://github.com/cilium/cilium/blob/2c59ba7dfaa5065e3b71814b97be7d217e07cda3/daemon/cmd/cni/config.go#L100-L111)仍是 `cniVersion: 0.3.1`。也就是说，二进制虽然实现了 `CHECK` 和 `STATUS`，默认配置的协议版本不会让运行时调用它们。只有配置版本、运行时和插件三者都支持，对应操作才会进入实际调用链。

这两个实现还共同说明一件事：常驻 Daemon 的内部健康接口不是新的 CNI 操作。Kube-OVN 的 Unix socket API 和 Cilium Agent API 都是插件自己的实现细节；对容器运行时暴露的边界仍是 CNI 的标准命令。

## 哪些网络行为不是独立的 CNI 操作

Kubernetes Service、NetworkPolicy、集群路由、隧道和 eBPF 数据面并没有各自对应一个标准 `CNI_COMMAND`。CNI 只规定运行时和插件之间的 attachment 生命周期接口，具体网络方案可以用不同方式实现其他能力：

- 在 `ADD` 时写入接口、路由或策略；
- 由节点上的常驻 Agent watch Kubernetes API，再持续维护数据面；
- 由控制器更新外部网络系统；
- 在 `DEL` 或 `GC` 时撤销节点和外部状态。

因此，Pod 的 `ADD` 成功只证明运行时接受了插件返回结果，不自动证明跨节点通信、Service 转发、DNS 或 NetworkPolicy 都已经正确工作。排查时仍要按接口、地址、路由、邻居、规则、隧道和控制面状态逐层确认。

## 用生命周期定位问题

遇到 CNI 错误时，可以先根据阶段缩小范围：

| 现象 | 优先检查 |
|---|---|
| Pod 长时间 `ContainerCreating`，事件含 `FailedCreatePodSandBox` | `ADD` 输入、插件日志、IPAM 容量、主机接口与外部控制面 |
| 删除 Pod 或 sandbox 失败 | `DEL` 是否拿到原配置和缓存结果、netns 消失后能否继续回收、插件链逆序清理 |
| `NetworkPluginNotReady` | CNI 配置是否加载、二进制是否存在、运行时 `Status` 细节、插件 `STATUS` 依赖 |
| Pod 已运行但怀疑网络状态漂移 | 运行时是否实际触发 `CHECK`；再独立检查接口、路由、规则和端到端流量 |
| 节点长期出现 IP 或规则泄漏 | `DEL` 失败记录、运行时缓存、是否实现并调度 `GC`、有效 attachment 集合是否完整 |
| 插件报不支持 CNI 版本 | 网络配置版本、运行时支持版本和插件 `VERSION` 结果 |

最后可以把六个操作压缩成三个层次：

1. `ADD`、`CHECK`、`DEL` 管理一个具体 attachment 的创建、校验和销毁。
2. `STATUS`、`GC` 管理网络插件整体的可服务状态和遗留资源。
3. `VERSION` 只负责协议能力协商，不改变网络状态。

在 Kubernetes 中，kubelet 看到的是 CRI 的 sandbox 和运行时状态，真正执行 CNI 操作的是容器运行时。把这个边界分清，再判断错误发生在创建、存续、删除还是运行时维护阶段，通常比笼统地说「CNI 插件有问题」更接近根因。

## 参考资料

- [Container Network Interface Specification v1.1.0](https://github.com/containernetworking/cni/blob/df52e9460d3a819920b63e28c4d5d6f12bed6758/SPEC.md)
- [Kubernetes：Network Plugins](https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/)
- [Kubernetes：Pod Lifecycle](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/)
- [containerd CRI architecture](https://github.com/containerd/containerd/blob/f6132dbe1f482cbe0aebc4bd3d8d7a184fb4a2aa/docs/cri/architecture.md)
- [containerd go-cni v1.1.14](https://github.com/containerd/go-cni/blob/v1.1.14/cni.go)
- [Kube-OVN CNI entrypoint：`cmd/cni/cni.go`](https://github.com/jimyag/kube-ovn/blob/a39b953338f43b4168d9b3368ad312bbe1fc96fb/cmd/cni/cni.go)
- [Cilium CNI entrypoint：`plugins/cilium-cni/cmd/cmd.go`](https://github.com/cilium/cilium/blob/2c59ba7dfaa5065e3b71814b97be7d217e07cda3/plugins/cilium-cni/cmd/cmd.go)

