Moby LibNetwork 远程驱动协议详解:remote driver 如何以插件形式接入容器网络
本文基于 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)):
Register()通过插件句柄注册一个回调(newPluginHandler),监听NetworkDriver这一端点类型(常量NetworkPluginEndpointType = "NetworkDriver",定义于 driverapi/driverapi.go)。当用户提到某个驱动时,插件系统会以plugins.GetAPI 加载该驱动,从而触发回调——这正是文档中“The callback is invoked when a driver is loaded”的实现位置。- 回调收到插件客户端后创建
driver代理实例,先通过GetCapabilitiesRPC 与远程进程协商能力;协商成功后调用RegisterDriver(name, d, capability)将其注册到NetworkController(Registerer接口定义见 driverapi/driverapi.go)。 - 若能力中
DataScope为global,还会额外调用RegisterNetworkAllocator(name, d)注册网络分配器,使其参与集群级 IPAM 分配(这引出了后文的AllocateNetwork方法)。
另外,从源码结构看,Register() 还支持从插件存储器中枚举已激活的 NetworkDriver 插件并逐一建立客户端,即 dockerd 重启后可重新接管已启用的远程驱动。
协议总览:JSON 载荷的 HTTP POST RPC
远程驱动协议是一组以 HTTP POST 形式发出的 RPC,请求/响应均为 JSON 载荷。代理(proxy)负责发起请求,远程驱动进程负责响应——通常是 JSON 响应体,部分情况下是空对象 {}。RPC 的报文大多是驱动方法参数的直接 JSON 翻译;少数例外是为容纳 InterfaceInfo、JoinInfo 等接口类型,以及 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.go 的 getCapabilities() 忠实实现了上述校验逻辑:Scope 与 ConnectivityScope 均通过 switch 语句严格限定为 global/local,ConnectivityScope 为空串时回退到 DataScope;GwAllocChecker 则存入驱动实例的 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.go 的 GetSkipGwAlloc():未声明 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.go 的 IPAMData 结构(AddressSpace、Pool *net.IPNet、Gateway *net.IPNet、AuxAddresses map[string]*net.IPNet),由 driver.go 的 CreateNetwork() 组装为 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 节点执行 CreateNetwork。driver.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 会把“请求给了非空值、驱动又返回非空值”判定为错误,并回滚该操作。响应中 MacAddress 与 Address/AddressIPv6 中至少一个(或两个)必须给出。
driver.go 的 CreateEndpoint() 实现了这一规则:请求时把 InterfaceInfo 中的地址/MAC 序列化为 EndpointInterface;收到响应后经 parseInterface()(driver.go)逐一做 CIDR/MAC 解析校验,再调用 SetMacAddress / SetIPAddress 写回端点接口——一旦原接口已有值而驱动试图修改,底层 InterfaceInfo 实现会返回 Forbidden 类错误。回滚逻辑体现在 defer 块中:只要 CreateEndpoint 最终失败,就自动补一次 DeleteEndpoint,并在错误信息中追加 ; rolled back 或 ; failed to roll back。测试用例 TestRollback(driver_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:接口移入沙箱后应添加的路由,可以有多条,顺序不限。RouteType为0时必须给出NextHop;RouteType为1时不给出NextHop,表示直连路由(connected route)。driver.go 的parseStaticRoutes()会对Destination做 CIDR 解析、对NextHop做 IP 解析,任一解析失败都会使整个 Join 失败。- 默认网关兜底:如果 Join 响应中既没有网关、也没有默认静态路由,LibNetwork 会为沙箱额外添加一个连接到默认网关网络(名为
docker_gwbridge的 bridge 网络)的接口,并把默认网关配置指向该 bridge 的地址——这保证了即使远程驱动不提供出口,容器也能获得基础出网能力。
driver.go 的 Join() 实现同样带有 defer 回滚:Join 后续处理失败时会自动调用 Leave() 撤销,与 CreateEndpoint 的回滚策略一致。
Leave
当代理被要求把端点从沙箱中移除时,远程进程收到发往 /NetworkDriver.Leave 的 POST:
{ "NetworkID": string, "EndpointID": string }
成功响应为空对象 {}。实现见 driver.go 的 Leave()。
发现通知与外部连通性编程
DiscoverNew / DiscoverDelete 通知
LibNetwork 监听内建的 Docker 发现(discovery)通知,并转发给感兴趣的驱动。
代理收到 DiscoverNew 通知时,远程进程会收到发往 /NetworkDriver.DiscoverNew 的 POST:
{
"DiscoveryType": int,
"DiscoveryData": { ... }
}
DiscoveryType 用数字表示发现类型,DiscoveryData 的结构由 DiscoveryType 决定。成功响应为空对象 {}。目前定义了节点发现(Node Discovery):DiscoveryType 值为 1,DiscoveryData 携带节点发现数据:
{
"DiscoveryType": 1,
"DiscoveryData": {
"Address": string,
"self": bool
}
}
DiscoverDelete 通知类似,发往 /NetworkDriver.DiscoverDelete,节点发现同样使用 DiscoveryType 值 1,DiscoveryData 携带待删除的节点数据,成功响应同为空对象。
实现见 driver.go:DiscoverNew / DiscoverDelete 都会先判断 dType != discoverapi.NodeDiscovery 并直接忽略,只有节点发现事件才会封装成 DiscoveryNotification 发出 RPC。
外部连通性编程(源码中的扩展)
当前源码中还实现了文档未覆盖的两个 RPC:ProgramExternalConnectivity 与 RevokeExternalConnectivity(请求/响应类型见 api.go)。从 driver.go 的实现看:LibNetwork 在端点被提升(或撤销)为网络网关时调用它们,向驱动编程/撤销“外部连通性”(如端口映射所需的 NAT 规则);驱动内部记录每个端点的网关状态(isGateway4/isGateway6)以跳过重复编程,并在收到 404(plugins.IsNotFound)时宽容处理——注释明确写道该方法“尚非强制要求支持”。此外,JoinResponse 还带有一个 DisableGatewayService 布尔字段,允许驱动在 Join 时请求禁用默认网关服务。若你正在实现一个面向最新 Moby 的远程驱动,建议关注这两个方法。
测试用例:协议行为的可验证依据
driver_test.go 用 httptest 服务器完整模拟了一个远程驱动进程,是理解协议的最佳参照:
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 |
NetworkPluginEndpointType、Capability、IPAMData 定义 |
daemon/libnetwork/driverapi/driverapi.go |
| 驱动注册调用点 | daemon/libnetwork/controller.go |
| 驱动方法语义的总体设计 | daemon/libnetwork/docs/design.md |
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00