首页
/ Moby LibNetwork 远程驱动协议详解:remote driver 如何以插件形式接入容器网络

Moby LibNetwork 远程驱动协议详解:remote driver 如何以插件形式接入容器网络

2026-09-04 19:59:43作者:齐添朝

本文基于 Moby 仓库中 daemon/libnetwork/docs/remote.md 的远程网络驱动协议规范展开,并结合 drivers/remote 的源码实现与测试用例,系统讲解 LibNetwork 的 remote driver 包如何与 Docker 插件体系集成、握手与能力协商的完整流程,以及 /NetworkDriver.* 各 RPC 的请求/响应报文格式。读完本文,你将能够理解远程网络驱动的注册机制,并具备依据该协议实现或对接一个外部网络驱动进程的能力。

remote driver 包的定位:为动态注册驱动提供代理

与其他内建驱动(bridge、macvlan 等)不同,drivers.remote 包并不直接实现某种具体的网络驱动,而是为远程驱动进程提供一个代理(proxy):远程进程通过 Docker 的插件机制(plugins 包)注册,并经由 HTTP RPC 与 LibNetwork 通信。驱动方法的语义遵循 LibNetwork 的总体设计(见 design.md),协议细节则由 remote driver 包自身定义。

这一设计的关键在于职责隔离:驱动注册机制的全部细节由 remote driver 包持有,不会把驱动层暴露给 LibNetwork 以北(North)的任何调用方。

从当前源码可以看到注册入口是 driver.go 中的 Register() 函数,LibNetwork 控制器在初始化时调用它(controller.go 中为 remotedriver.Register(&c.drvRegistry, c.cfg.PluginGetter)):

  1. Register() 通过插件句柄注册一个回调(newPluginHandler),监听 NetworkDriver 这一端点类型(常量 NetworkPluginEndpointType = "NetworkDriver",定义于 driverapi/driverapi.go)。当用户提到某个驱动时,插件系统会以 plugins.Get API 加载该驱动,从而触发回调——这正是文档中“The callback is invoked when a driver is loaded”的实现位置。
  2. 回调收到插件客户端后创建 driver 代理实例,先通过 GetCapabilities RPC 与远程进程协商能力;协商成功后调用 RegisterDriver(name, d, capability) 将其注册到 NetworkControllerRegisterer 接口定义见 driverapi/driverapi.go)。
  3. 若能力中 DataScopeglobal,还会额外调用 RegisterNetworkAllocator(name, d) 注册网络分配器,使其参与集群级 IPAM 分配(这引出了后文的 AllocateNetwork 方法)。

另外,从源码结构看,Register() 还支持从插件存储器中枚举已激活的 NetworkDriver 插件并逐一建立客户端,即 dockerd 重启后可重新接管已启用的远程驱动。

协议总览:JSON 载荷的 HTTP POST RPC

远程驱动协议是一组以 HTTP POST 形式发出的 RPC,请求/响应均为 JSON 载荷。代理(proxy)负责发起请求,远程驱动进程负责响应——通常是 JSON 响应体,部分情况下是空对象 {}。RPC 的报文大多是驱动方法参数的直接 JSON 翻译;少数例外是为容纳 InterfaceInfoJoinInfo 等接口类型,以及 JSON 序列化表现不佳的类型(如 net.IPNet,会序列化为 CIDR 字符串)。

错误语义

协议对错误有明确的三层约定:

  • 语法/解码错误:远程进程无法解码请求、或检测到 HTTP 请求或载荷存在语法问题时,必须以 HTTP 错误状态码(4xx 或 5xx)响应。
  • 未知 URI:远程进程的 HTTP 服务收到未知 URI 的请求时,应返回 404 Not Found。这样 LibNetwork 就能识别“远程驱动尚未实现某个新增方法”的情况,而不会把请求判定为失败——这是协议向后兼容的关键设计。
  • 业务错误:请求可以解码但操作无法完成时,必须返回形如下面的响应:
{
    "Err": string
}

由于该字符串可能出现在日志中,不应包含敏感信息。

在源码中,所有响应类型都内嵌了 api.go 中定义的 Response 结构体(含 Err 字段并实现 GetError());driver.go 的统一调用入口 call() 会在 RPC 成功后检查 Err 字段,非空时包装为 remote: <err> 错误返回。

握手(Handshake)

驱动被加载时,远程进程会收到发往 /Plugin.Activate 的 HTTP POST(无载荷),必须响应一份能力清单:

{
    "Implements": ["NetworkDriver"]
}

列表中允许出现其他条目;"NetworkDriver" 表示该插件应作为网络驱动注册到 LibNetwork。测试用例 driver_test.go 中正是这样模拟握手的:

mux.HandleFunc("/Plugin.Activate", func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", plugins.VersionMimetype)
    fmt.Fprintf(w, `{"Implements": ["%s"]}`, driverapi.NetworkPluginEndpointType)
})

能力协商(Set capability)

握手之后,远程驱动会收到发往 /NetworkDriver.GetCapabilities 的 POST(无载荷),响应形如:

{
    "Scope":             "local",
    "ConnectivityScope": "global",
    "GwAllocChecker":    false
}

各字段的含义与约束:

  • Scope:只允许 "local""global",表示该驱动网络的资源分配只能在节点本地完成,还是可以跨集群节点全局进行。任何其他值都会导致驱动注册失败并返回错误。
  • ConnectivityScope:只允许 "local""global",表示驱动网络提供的连通范围仅限本节点还是跨集群。若该字段缺失,LibNetwork 会将其设置为与 Scope 相同的值。
  • GwAllocChecker:可省略;若为 true,代理会在请求 /NetworkDriver.CreateNetwork 之前先请求 /NetworkDriver.GwAllocCheck

driver.gogetCapabilities() 忠实实现了上述校验逻辑:ScopeConnectivityScope 均通过 switch 语句严格限定为 global/localConnectivityScope 为空串时回退到 DataScopeGwAllocChecker 则存入驱动实例的 gwAllocChecker 字段。

网络生命周期:GwAllocCheck、CreateNetwork、DeleteNetwork

网关分配检查(Gateway Allocation Check)

若代理在能力协商中声明了 "GwAllocChecker": true,远程进程会收到发往 /NetworkDriver.GwAllocCheck 的 POST:

{
    "Options": { ... }
}

其中 Options 与随后 /NetworkDriver.CreateNetwork 请求中发送的 map 完全相同。响应必须为:

{
    "SkipIPv4": false,
    "SkipIPv6": false
}
  • "SkipIPv4": true 且 IPv4 IPAM 配置中未包含 Gateway 地址时,LibNetwork 不会为网络网关分配地址;
  • "SkipIPv6": true 同理,用于跳过 IPv6 网关地址分配。

实现见 driver.goGetSkipGwAlloc():未声明 GwAllocChecker 能力时直接返回 (false, false),不发起 RPC;否则发送 GwAllocCheckerRequest 并解析 SkipIPv4/SkipIPv6 两个布尔字段(定义于 api.go)。

创建网络(Create network)

代理被要求创建网络时,远程进程收到发往 /NetworkDriver.CreateNetwork 的 POST:

{
    "NetworkID": string,
    "IPv4Data": [
        {
            "AddressSpace": string,
            "Pool": "ipv4-cidr-string",
            "Gateway": "ipv4-cidr-string",
            "AuxAddresses": {
                "<identifier1>": "<ipv4-address1>",
                "<identifier2>": "<ipv4-address2>"
            }
        }
    ],
    "IPv6Data": [ ... 同上,IPv6 形式 ... ],
    "Options": { ... }
}

字段含义:

  • NetworkID:由 LibNetwork 生成的网络唯一标识。
  • Options:LibNetwork 传给代理的任意 map(用户 docker network create --opt 提供的选项最终会走到这里)。
  • IPv4Data / IPv6Data:用户配置、由 IPAM 驱动管理的寻址数据,网络驱动应遵守这些数据:
    • AddressSpace:唯一字符串,表示一个隔离的 IP 寻址空间;
    • Pool:CIDR 格式(address/mask)表示的 IP 地址范围。由于容器 IP 由 IPAM 驱动分配,网络驱动可利用此信息做网络接线(plumbing);
    • Gateway:可选,IPAM 驱动可为 Pool 子网提供的 CIDR 格式网关地址;
    • AuxAddresses:用户提供的、带标识符的预分配 IP 列表,供驱动在需要特定 IP 时使用。

成功响应为空对象 {}

在 Go 侧,这些数据对应 driverapi/driverapi.goIPAMData 结构(AddressSpacePool *net.IPNetGateway *net.IPNetAuxAddresses map[string]*net.IPNet),由 driver.goCreateNetwork() 组装为 CreateNetworkRequest 发出 RPC。

删除网络(Delete network)

删除网络时,远程进程收到发往 /NetworkDriver.DeleteNetwork 的 POST:

{ "NetworkID": string }

成功响应为空对象 {}

源码中的扩展:AllocateNetwork / FreeNetwork

需要指出的是,当前源码中的协议已超出文档记载:api.go 定义了 AllocateNetworkRequest / FreeNetworkRequest,其请求体与 CreateNetworkRequest 相同(NetworkID + Options + IPv4Data/IPv6Data),但 AllocateNetworkResponse 额外携带一个字符串 map Options,注释说明该 map 会随 libnetwork agent 的 CreateNetworkRequest 一并下发。从源码结构看,这一对方法服务于 Scope: global 的驱动:由管理器(manager)节点在集群层面做网络分配,再将分配结果通过 Options 传递给各 agent 节点执行 CreateNetworkdriver.NetworkAllocate() / NetworkFree()driver.go)即为其客户端实现,并在能力为 global 时通过 RegisterNetworkAllocator 被接入 IPAM 分配链路。

端点生命周期:CreateEndpoint、EndpointOperInfo、DeleteEndpoint

创建端点(Create endpoint)

代理被要求创建端点时,远程进程收到发往 /NetworkDriver.CreateEndpoint 的 POST:

{
    "NetworkID": string,
    "EndpointID": string,
    "Options": { ... },
    "Interface": {
        "Address": string,
        "AddressIPv6": string,
        "MacAddress": string
    }
}
  • NetworkID 是端点所属网络的生成标识;EndpointID 是端点的生成标识;
  • Options 是传给代理的任意 map;
  • Interface 各字段可以为空,Interface 本身也可以为空。Address 为 CIDR 表示的 IPv4 地址与子网(如 "192.168.34.12/16"),AddressIPv6 为 CIDR 表示的 IPv6 地址,MacAddress 为 MAC 地址字符串(如 "6e:75:32:60:44:c9")。

成功响应形如:

{
    "Interface": {
        "Address": string,
        "AddressIPv6": string,
        "MacAddress": string
    }
}

关于返回值有一条严格规则:如果请求中 Interface 非空,响应中的 Interface 必须为空。LibNetwork 会把“请求给了非空值、驱动又返回非空值”判定为错误,并回滚该操作。响应中 MacAddressAddress/AddressIPv6 中至少一个(或两个)必须给出。

driver.goCreateEndpoint() 实现了这一规则:请求时把 InterfaceInfo 中的地址/MAC 序列化为 EndpointInterface;收到响应后经 parseInterface()driver.go)逐一做 CIDR/MAC 解析校验,再调用 SetMacAddress / SetIPAddress 写回端点接口——一旦原接口已有值而驱动试图修改,底层 InterfaceInfo 实现会返回 Forbidden 类错误。回滚逻辑体现在 defer 块中:只要 CreateEndpoint 最终失败,就自动补一次 DeleteEndpoint,并在错误信息中追加 ; rolled back; failed to roll back。测试用例 TestRollbackdriver_test.go)专门验证了该回滚路径会被触发。

端点运行信息(Endpoint operational info)

代理被要求获取端点的“运行信息”时,远程进程收到发往 /NetworkDriver.EndpointOperInfo 的 POST:

{ "NetworkID": string, "EndpointID": string }

响应形如:

{ "Value": { ... } }

Value 是一个任意(可为空)的 map。

删除端点(Delete endpoint)

删除端点时,远程进程收到发往 /NetworkDriver.DeleteEndpoint 的 POST:

{ "NetworkID": string, "EndpointID": string }

成功响应为空对象 {}

Join / Leave:端点与沙箱的衔接

Join

当沙箱(sandbox,即容器的网络命名空间视图)被赋予一个端点时,远程进程收到发往 NetworkDriver.Join 的 POST:

{
    "NetworkID": string,
    "EndpointID": string,
    "SandboxKey": string,
    "Options": { ... }
}

SandboxKey 标识沙箱,Options 是传给代理的任意 map。响应必须具有如下结构:

{
    "InterfaceName": {
        "SrcName": string,
        "DstName": string,
        "DstPrefix": string
    },
    "Gateway": string,
    "GatewayIPv6": string,
    "StaticRoutes": [
        {
            "Destination": string,
            "RouteType": int,
            "NextHop": string
        }
    ]
}

字段语义:

  • Gateway / GatewayIPv6:可选,分别为字符串形式的 IPv4 / IPv6 网关地址(如 "192.168.0.1""fe80::7809:baff:fec6:7744")。
  • InterfaceName:描述应当被 LibNetwork 移入沙箱的真实操作系统级接口。SrcName 是远程进程创建的 OS 级接口名;DstName 可选,指定接口移入沙箱后的确切名称;若 DstName 为空,则以 DstPrefix 为前缀生成接口名,由 LibNetwork 追加索引避免与其他接口重名。driver.go 的注释进一步说明:DstName 是驱动响应容器内自定义接口名(如通过 com.docker.network.endpoint.ifname 端点选项,经 CreateEndpoint 的 options 传给驱动)的方式,为空时回退到基于 DstPrefix 的生成规则;测试 TestRemoteDriverJoinDstName 覆盖了两条路径。
  • StaticRoutes:接口移入沙箱后应添加的路由,可以有多条,顺序不限。RouteType0 时必须给出 NextHopRouteType1 时不给出 NextHop,表示直连路由(connected route)。driver.goparseStaticRoutes() 会对 Destination 做 CIDR 解析、对 NextHop 做 IP 解析,任一解析失败都会使整个 Join 失败。
  • 默认网关兜底:如果 Join 响应中既没有网关、也没有默认静态路由,LibNetwork 会为沙箱额外添加一个连接到默认网关网络(名为 docker_gwbridge 的 bridge 网络)的接口,并把默认网关配置指向该 bridge 的地址——这保证了即使远程驱动不提供出口,容器也能获得基础出网能力。

driver.goJoin() 实现同样带有 defer 回滚:Join 后续处理失败时会自动调用 Leave() 撤销,与 CreateEndpoint 的回滚策略一致。

Leave

当代理被要求把端点从沙箱中移除时,远程进程收到发往 /NetworkDriver.Leave 的 POST:

{ "NetworkID": string, "EndpointID": string }

成功响应为空对象 {}。实现见 driver.goLeave()

发现通知与外部连通性编程

DiscoverNew / DiscoverDelete 通知

LibNetwork 监听内建的 Docker 发现(discovery)通知,并转发给感兴趣的驱动。

代理收到 DiscoverNew 通知时,远程进程会收到发往 /NetworkDriver.DiscoverNew 的 POST:

{
    "DiscoveryType": int,
    "DiscoveryData": { ... }
}

DiscoveryType 用数字表示发现类型,DiscoveryData 的结构由 DiscoveryType 决定。成功响应为空对象 {}。目前定义了节点发现(Node Discovery)DiscoveryType 值为 1DiscoveryData 携带节点发现数据:

{
    "DiscoveryType": 1,
    "DiscoveryData": {
        "Address": string,
        "self": bool
    }
}

DiscoverDelete 通知类似,发往 /NetworkDriver.DiscoverDelete,节点发现同样使用 DiscoveryType1DiscoveryData 携带待删除的节点数据,成功响应同为空对象。

实现见 driver.goDiscoverNew / DiscoverDelete 都会先判断 dType != discoverapi.NodeDiscovery 并直接忽略,只有节点发现事件才会封装成 DiscoveryNotification 发出 RPC。

外部连通性编程(源码中的扩展)

当前源码中还实现了文档未覆盖的两个 RPC:ProgramExternalConnectivityRevokeExternalConnectivity(请求/响应类型见 api.go)。从 driver.go 的实现看:LibNetwork 在端点被提升(或撤销)为网络网关时调用它们,向驱动编程/撤销“外部连通性”(如端口映射所需的 NAT 规则);驱动内部记录每个端点的网关状态(isGateway4/isGateway6)以跳过重复编程,并在收到 404plugins.IsNotFound)时宽容处理——注释明确写道该方法“尚非强制要求支持”。此外,JoinResponse 还带有一个 DisableGatewayService 布尔字段,允许驱动在 Join 时请求禁用默认网关服务。若你正在实现一个面向最新 Moby 的远程驱动,建议关注这两个方法。

测试用例:协议行为的可验证依据

driver_test.gohttptest 服务器完整模拟了一个远程驱动进程,是理解协议的最佳参照:

  • setupPlugin() 在插件目录写入 spec 文件并注册 /Plugin.Activate 握手路由,模拟驱动的完整加载过程;handle() 辅助函数按 /NetworkDriver.<method> 模式注册各 RPC 端点(driver_test.go)。
  • TestRemoteDriver 走通全链路:GetCapabilities(含 GwAllocChecker: true)→ GwAllocCheck → CreateNetwork → CreateEndpoint(校验 MAC/IPv4/IPv6 写回)→ Join(校验 Gateway、GatewayIPv6、InterfaceName、StaticRoutes)→ EndpointOperInfo → Leave → DeleteEndpoint → DeleteNetwork → DiscoverNew/DiscoverDelete。
  • TestGetEmptyCapabilities / TestGetInvalidCapabilities 验证了文档中“Scope 非法值导致注册失败”的约定;TestGetExtraCapabilities 验证多余字段(如 "foo": "bar")被安全忽略。
  • TestDriverError 验证 {"Err": "..."} 响应会被转换为 Go 错误返回。
  • TestRollback 验证 CreateEndpoint 失败后 DeleteEndpoint 回滚被触发。

关键源码路径速查

关注点 路径
协议规范文档(本文主体) daemon/libnetwork/docs/remote.md
驱动代理实现(Register、call、Join 回滚等) daemon/libnetwork/drivers/remote/driver.go
全部请求/响应类型定义 daemon/libnetwork/drivers/remote/api/api.go
协议测试(httptest 模拟远程驱动) daemon/libnetwork/drivers/remote/driver_test.go
NetworkPluginEndpointTypeCapabilityIPAMData 定义 daemon/libnetwork/driverapi/driverapi.go
驱动注册调用点 daemon/libnetwork/controller.go
驱动方法语义的总体设计 daemon/libnetwork/docs/design.md
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
980
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384