Skip to content

WireGuard 插件

wireguard 插件为 Umoo 管理的设备提供自动化、多租户网状 VPN 网络。每个设备组都可以加入专属的 WireGuard 网络。平台自动处理密钥对生成、IP 分配、STUN NAT 穿透及设备配置下发——服务端无需手动操作 wg 工具。


目录

  1. 架构概览
  2. IP 地址分配
  3. 设备端插件
  4. 服务端插件
  5. 总线消息协议
  6. ConnectRPC API
  7. 租户配置
  8. 运维说明

架构概览

插件分布在两个通过消息总线通信的进程中:

┌────────────────────────────────────────────────────────────────────────┐
│  后端(umoo server)                                                    │
│                                                                        │
│  ┌──────────────────────┐     ┌───────────────────────────────────┐   │
│  │  WireGuardService    │     │  AdminService RPCs                │   │
│  │  ─────────────────── │     │  ─────────────────────────────── │   │
│  │  • CreateMeshNetwork │◄────│  CreateWireGuardNetwork           │   │
│  │  • IP 分配            │     │  EnrollGroupInNetwork             │   │
│  │  • 节点注册           │     │  AddDeviceToNetwork               │   │
│  │  • 配置构建           │     │  SetPeerRole / AssignRelay        │   │
│  │  • 配置分发           │     │  ListWireGuardPeers               │   │
│  └──────────┬───────────┘     └───────────────────────────────────┘   │
│             │ NATS 总线                                                │
└─────────────┼──────────────────────────────────────────────────────────┘

    ╔═════════╧═════════╗
    ║    消息总线         ║
    ║  (NATS / local)    ║
    ╚═════════╤═════════╝

┌─────────────┼──────────────────────────────────────────────────────────┐
│  设备代理(umoo-agent)          │                                    │
│                                    │                                    │
│  ┌─────────────────────────────────▼───────────────────────────────┐   │
│  │  WireGuardPlugin(代理端)                                       │   │
│  │  ──────────────────────────────────────────────────────────     │   │
│  │  • 订阅:cmd.plugin.wireguard.join                               │   │
│  │          cmd.plugin.wireguard.full_configs                      │   │
│  │  • 发布:evt.plugin.wireguard.pubkey(密钥 + 端点)               │   │
│  │          evt.plugin.wireguard.endpoint(STUN 结果)              │   │
│  │  • 管理:wg0、wg1、……(每个已加入网络一个)                       │   │
│  └─────────────────────────────────────────────────────────────────┘   │
└────────────────────────────────────────────────────────────────────────┘

每设备每网络一个 WireGuard 接口。 如果一台设备属于三个组网络,则运行三个独立的 wgN 接口,各自拥有独立的密钥、IP 和节点列表。


IP 地址分配

IPAllocatorinternal/backend/plugins/wireguard/ip_allocator.go)完全在内存中管理每网络 IP 池,采用顺序分配和显式释放策略。

工作原理

CIDR "100.64.1.0/24"
  ├── .0   保留(网络地址,跳过)
  ├── .1   → 第一次 Allocate() 调用
  ├── .2   → 第二次
  │   ...
  ├── .254 → 第 254 次
  └── .255 保留(广播地址,跳过)
  • 顺序分配,非随机nextHost 计数器递增并循环。这使节点 IP 在开发阶段可预期,但计数器会在服务器重启后重置。
  • 重启注意事项:分配器仅存在于内存中。重启时,PreloadAllocator 会从各网络存储的 subnet_cidr 按网络调用。已分配的 IP 不会从数据库重播——现有节点保留 allocated_ip 列的值,但内存计数器从 .1 重新开始。若新节点加入一个已有 .1 的网络,分配器将再次分配 .1,导致 UpsertPeer 调用冲突。生产环境修复方案PreloadAllocator 应通过查询 wg_peers 将所有现有节点 IP 预先标记为已分配。
  • 最小子网/30(2 个可用主机)。分配器拒绝更小的子网。
  • IPv6:不支持——仅接受 IPv4 CIDR。

默认 CIDR

推荐默认使用 100.64.0.0/10(RFC 6598 共享地址空间)。该范围:

  • 在公共互联网上不可路由
  • 不被家用路由器(192.168.x)或云 VPC(10.x)使用
  • 不与 Docker 桥接范围(172.17.0.0/16)冲突

各组网络应从该范围内划分 /24 子网,例如:

组 A → 100.64.1.0/24   (254 台设备)
组 B → 100.64.2.0/24
...
组 N → 100.64.255.0/24

设备端插件

源码:internal/agent/plugins/wireguard/

代理插件管理一组 networkState 条目(每个已加入网络一个),每个条目映射到一个 WireGuard 接口(wg0wg1、……)。接口名称从进程内计数器顺序分配——第一个网络为 wg0,第二个为 wg1,以此类推。

平台实现(Go 库)

WGCommandRunner 接口抽象了所有平台操作。生产实现(nativeWGRunner)使用 Go 库而非调用 wg/wg-quick 命令行工具:

操作旧方式(CLI)新方式(Go 库)
密钥生成wg genkey / wg pubkeywgtypes.GeneratePrivateKey()
节点配置/加密wg set <iface> peer ...wgctrl.Client.ConfigureDevice()
接口统计wg show <iface> dumpwgctrl.Client.Device()
接口创建(Linux)wg-quick upnetlink.LinkAdd(&GenericLink{LinkType: "wireguard"})
IP 分配(Linux)通过 wg-quick 执行 ip addr addnetlink.AddrAdd()
路由管理(Linux)通过 wg-quick 执行 ip route addnetlink.RouteAdd()
接口销毁(Linux)wg-quick downnetlink.LinkDel()(自动删除路由)
接口创建(macOS)wg-quick upwireguard-go <iface> 子进程
IP 分配(macOS)通过 wg-quick 执行 ifconfigexec ifconfig
路由管理(macOS)通过 wg-quick 执行 route addexec route -q -n add -inet
IP 转发sysctlsysctl(不变——OS 工具,非 WireGuard 专有)
NAT 伪装(Linux)iptablesiptables(不变)
NAT 伪装(macOS)iptables存根 + 警告(请手动使用 pf)

构建标签强制平台隔离:

  • wg_runner_common.go — 所有平台(wgctrl 调用、密钥操作、INI 解析器)
  • wg_runner_linux.go//go:build linux(netlink)
  • wg_runner_darwin.go//go:build darwin(wireguard-go、ifconfig、route)

WireGuard 配置生命周期

当服务端推送 full_configs 命令时,代理执行:

1. WriteConfig("/etc/wireguard/wg0.conf", content)
   └── 将 INI 内容解析为 parsedWGConfig
       缓存在 nativeWGRunner.pending["wg0"] 中
       (不写入磁盘文件)

2. QuickDown("wg0")   ← 如果接口已在运行
   └── netlink.LinkDel / 终止 wireguard-go 进程

3. QuickUp("wg0")
   └── [Linux]  netlink.LinkAdd(wireguard 类型)
               netlink.AddrAdd(10.0.0.5/24)
               netlink.LinkSetUp
               wgctrl.ConfigureDevice(私钥、监听端口、节点)
               为每个节点 AllowedIP 执行 netlink.RouteAdd
   └── [macOS]  wireguard-go wg0(创建 utun)
               sleep 100ms
               ifconfig wg0 <ip> <ip> netmask <mask> broadcast <bcast>
               ifconfig wg0 up
               wgctrl.ConfigureDevice
               为每个节点 AllowedIP 执行 route add

全隧道路由(0.0.0.0/0)将被跳过并记录警告。服务端在正常网状配置中不会发送这些路由——仅分发明确的节点 /32 路由和出口子网。

STUN NAT 穿透

应用每个网络配置后,插件会启动一个 goroutine 探测 STUN 服务器:

go
go p.probeAndReportSTUN(nc.NetworkID, nc.StunServers)

ProbeSTUN 遍历已配置的 STUN 服务器(如果未配置则回退到 Google/Cloudflare STUN),通过 UDP 发送 RFC 5389 绑定请求。成功后,解析 XOR-MAPPED-ADDRESS 属性以获取设备的公网 IP:port

结果以包含端点的 evt.plugin.wireguard.pubkey 事件发布。服务端随后更新节点的 endpoint 列并触发全网配置重新分发,使所有节点获取到该设备的最新直连路由。

STUN 为尽力而为:若所有探测均失败,设备进入仅中继模式,endpoint 字段保持为空。没有端点的设备仍可在分配了中继节点的情况下通信。


服务端插件

源码:internal/backend/plugins/wireguard/

服务层

WireGuardService 是核心协调器,主要职责:

网络生命周期:

  • CreateMeshNetwork — 验证 CIDR,创建数据库行,初始化内存 IPAllocator
  • DeleteNetwork — 级联删除数据库,从内存中移除分配器
  • GetDomainNetwork / ListDomainNetworks — 透传到 repository

节点生命周期:

  • AddDeviceToNetwork — 向设备发布 cmd.plugin.wireguard.join;设备生成密钥对后通过 evt.plugin.wireguard.pubkey 上报
  • HandlePubkeyReport — 幂等注册:首次调用时分配 IP,后续调用时更新端点,然后调用 DistributeToNetwork
  • HandleEndpointReport — 仅更新端点(节点已注册),重新分发
  • RemovePeer — 删除数据库行,将 IP 释放回分配器

配置构建(BuildNetworkConfig):

为网络中的指定设备构建 WireGuardNetworkConfig,包含:

  • Address:此设备的 VPN IP 及前缀长度(例如 100.64.1.5/24
  • ListenPort:来自网络记录(默认 51820)
  • StunServers:传递给设备用于端点发现
  • Peers:每个其他已加入设备一条记录,包含:
    • AllowedIPs:节点的 /32 网状 IP,若节点为出口网关则附加出口子网
    • Endpoint:若已知则为直连 IP:port,若存在中继分配则为中继端点
    • Keepalive:通过中继路由或目标为中继节点时为 25 秒
    • ViaRelay:直连端点不可用时为 true

配置分发

DistributeToDeviceDistributeToNetwork 是两个分发入口点。

DistributeToNetwork(networkID)
  └── ListPeers(networkID)
  └── for each peer → DistributeToDevice(deviceID)

DistributeToDevice(deviceID)
  └── ListNetworksByDevice(deviceID)     ← 该设备所属的所有网络
  └── for each network → BuildNetworkConfig(network, deviceID)
  └── 发布 cmd.plugin.wireguard.full_configs
      主题:"device/{deviceID}/cmd.plugin.wireguard.full_configs"
      载荷:{ networks: [...] }

始终推送完整配置。 服务端从不发送增量节点差异。每次配置推送都包含所有网络和所有节点,使代理在配置方面保持无状态——它始终拥有完整的全局视图。

DistributeToNetwork 在任何影响连通性的状态变更后自动调用:节点注册、端点更新、节点角色变更、中继分配/移除。

中继系统

WireGuard 要求双方都知道对方的公网 IP 才能建立直连。处于严格 NAT 后面的设备(对称 NAT、无端口映射的 CGNAT)无法被直接访问。

节点中继(网络内):

  1. 将某设备指定为中继:SetPeerRole(isRelay=true)。该设备必须有可达的公网端点。
  2. 将中继分配给受 NAT 限制的节点:AssignRelay(networkID, peerDeviceID, relayDeviceID)
  3. 下次配置分发时,所有设备将看到:
    • 中继节点设有 PersistentKeepalive = 25
    • 受 NAT 限制的节点标记 ViaRelay = true 并使用中继端点

受 NAT 限制的设备无法接受入站连接,但可以建立出站连接。双方均设置 PersistentKeepalive 后,中继节点将维持持久隧道供受限设备使用。

平台中继(跨租户,由 super_admin 管理):

平台中继(wg_platform_relays)是可服务于多个租户的基础设施级中继服务器。通过 CreatePlatformRelay(仅 super_admin)注册,并分配给租户网络。分配到网络的功能(AssignPlatformRelayToNetwork)当前标记为 CodeUnimplemented,属于计划中的功能。

出口网关

出口网关节点将发往外部子网(如本地网络)的流量通过指定设备路由。

  1. 在网关设备上执行 SetPeerRole(isEgress=true, egressSubnets=["192.168.10.0/24"])
  2. 下次配置分发时,所有其他节点将获得指向网关网状 IP 的 192.168.10.0/24AllowedIPs 条目。
  3. 代理插件在网关设备的 WireGuard 接口上应用 EnableIPForwarding + ConfigureMasquerade

总线消息协议

所有 WireGuard 插件消息使用以下主题结构:

命令(服务端 → 设备)

主题方向载荷
device/{deviceID}/cmd.plugin.wireguard.join服务端 → 设备{"network_id": "<uuid>"}
device/{deviceID}/cmd.plugin.wireguard.full_configs服务端 → 设备见下方

full_configs 载荷:

json
{
  "networks": [
    {
      "network_id":  "uuid",
      "address":     "100.64.1.5/24",
      "listen_port": 51820,
      "stun_servers": ["stun.l.google.com:19302"],
      "is_relay":    false,
      "is_egress":   false,
      "egress_subnets": [],
      "peers": [
        {
          "public_key":  "base64...",
          "allowed_ips": ["100.64.1.2/32"],
          "endpoint":    "203.0.113.10:51820",
          "keepalive":   0
        },
        {
          "public_key":  "base64...",
          "allowed_ips": ["100.64.1.3/32"],
          "via_relay":   true,
          "endpoint":    "203.0.113.99:51820",
          "keepalive":   25
        }
      ]
    }
  ]
}

事件(设备 → 服务端)

主题方向载荷
evt.plugin.wireguard.pubkey设备 → 服务端{"network_id": "uuid", "public_key": "base64...", "endpoint": "1.2.3.4:51820"}
evt.plugin.wireguard.endpoint设备 → 服务端{"network_id": "uuid", "endpoint": "1.2.3.4:51820"}

服务端在 WireGuardServerPlugin.Register() 中订阅 evt.plugin.wireguard.pubkey 并路由到 HandlePubkeyReportendpoint 字段为可选——STUN 失败时不存在该字段。


ConnectRPC API

所有 RPC 均需要 JWT 认证 + X-Tenant-ID 请求头(另有说明除外)。

网络管理

RPC所需权限说明
CreateWireGuardNetworktenant_admin+创建独立网状网络
GetWireGuardNetworkJWT按 ID 获取网络
ListWireGuardNetworksJWT列出租户的所有网络
DeleteWireGuardNetworktenant_admin+删除网络及所有节点
EnrollGroupInNetworktenant_admin+原子性地创建网络并加入组内所有当前设备

CreateWireGuardNetwork 请求:

json
{
  "tenant_id":   "uuid",
  "name":        "production-mesh",
  "subnet_cidr": "100.64.1.0/24",
  "group_id":    "uuid",       // 可选
  "listen_port": 51820         // 0 → 默认 51820
}

EnrollGroupInNetwork 是组级网状设置的推荐路径。它将:

  1. 使用组 ID 调用 CreateMeshNetwork
  2. 获取所有当前组成员(最多 1000 个)
  3. 为每个成员调用 AddDeviceToNetwork,发布 join 命令

入组加入该组的设备须通过 AddDeviceToNetwork 单独处理。

节点管理

RPC所需权限说明
AddDeviceToNetworkoperator+向设备发布加入命令
RemoveDeviceFromNetworkoperator+移除节点,释放 IP
ListWireGuardPeersJWT列出网络中的所有节点
SetPeerRoleoperator+设置中继/出口标志及出口子网
AssignRelayoperator+为受 NAT 限制的节点分配中继
RemoveRelayAssignmentoperator+移除中继分配

SetPeerRole 请求:

json
{
  "tenant_id":        "uuid",
  "network_id":       "uuid",
  "device_id":        "uuid",
  "is_relay":         true,
  "is_egress_gateway": false,
  "egress_subnets":   []
}

更改任何角色标志将立即触发 DistributeToNetwork

平台中继管理(仅 super_admin)

RPC说明
CreatePlatformRelay注册新的基础设施中继服务器
ListPlatformRelays列出所有平台中继
DeletePlatformRelay移除平台中继
AssignPlatformRelayToNetwork(尚未实现)
RemovePlatformRelayFromNetwork(尚未实现)

租户配置

TenantConfig.WireGuardConfig 字段(TenantWireGuardConfig proto 消息)提供每租户默认配置:

字段类型默认值说明
mesh_enabledboolfalse是否为此租户启用 WireGuard 网状网络
ip_rangestring100.64.0.0/10用于分配每组 /24 子网的默认 CIDR 池
default_listen_portint3251820新网络的默认 UDP 监听端口
default_dnsstring可选 DNS 服务器,推送到设备
default_keepaliveint320所有节点的 PersistentKeepalive(0 = 禁用,中继路由时除外)
nat_traversal_enabledbooltrue是否启用 STUN 探测
stun_servers[]stringGoogle/Cloudflare STUN每次 full_configs 推送时附带的 STUN 服务器列表

这些设置存储在 tenants.settings JSONB 列中,通过 UpdateTenantConfig 更新。目前这些设置尚不自动应用于新网络创建——运营人员今天必须在 CreateWireGuardNetwork 中明确传递相应值。


运维说明

已知限制

IP 分配器非崩溃安全。 服务端重启后,内存分配器从 .1 重新开始。现有节点在数据库中保留其已分配的 IP,但计数器不知道哪些地址已被占用。若新节点在分配器遇到冲突之前加入,将分配出重复地址。修复方案:启动时按网络将所有现有节点 IP 预加载到分配器中。

full_configs 为发布即忘。 服务端发布到总线后不等待设备 ACK。若设备离线,消息将丢失(NATS 最多一次)。当设备重连且状态机进入 Syncing 状态时,不会自动重新请求网状配置——服务端必须重新分发。这是一个缺口:设备重连应触发 DistributeToDevice

代理不上报握手信息。 数据库中存在 last_handshake_at 列,但当前代理从不写入。代理插件仅通过 ShowDump 读取握手时间用于指标导出,不上报回服务端。

macOS 伪装未实现。 ConfigureMasquerade 在 Darwin 上是空操作并记录警告。在 macOS 上运行出口网关的运营人员必须手动配置 pf

全隧道路由被跳过。 AllowedIPs 中的 0.0.0.0/0 被代理静默丢弃并记录警告。服务端在正常配置中不发送这类路由,但自定义配置可能触发此问题。

推荐部署模式

1. 创建设备组(例如"production-nodes")
2. 调用 EnrollGroupInNetwork:
   - name: "production-mesh"
   - subnet_cidr: "100.64.1.0/24"
   - group_id: <production-nodes UUID>
3. 将一台设备指定为中继(理想情况下为具有静态 IP 的云虚拟机):
   对该设备执行 SetPeerRole(is_relay=true)
4. 对于 STUN 失败的设备(对称 NAT):
   AssignRelay(peerDeviceID=<NAT 设备>, relayDeviceID=<中继设备>)
5. 若需访问本地子网,指定一台网关设备:
   SetPeerRole(is_egress_gateway=true, egress_subnets=["192.168.0.0/16"])

加入后添加设备

AddDeviceToNetwork(network_id, device_id)

此操作向设备发布 cmd.plugin.wireguard.join。设备生成密钥对后发布 evt.plugin.wireguard.pubkey。服务端注册节点、分配 IP,并向网络中的所有节点重新分发 full_configs

监控

代理的 CollectMetrics 方法(由遥测插件调用)导出每节点 WireGuard 统计信息:

指标说明
wg_peer_bytes_rx从该节点接收的字节数
wg_peer_bytes_tx向该节点发送的字节数
wg_peer_last_handshake最后一次成功握手的 Unix 时间戳

标签:network_idpeer_pubkey

这些指标通过 wgctrl.Client.Device() 采集(Linux 上有 CAP_NET_ADMIN 时无需 root;WireGuard 内核模块通过 generic netlink 暴露统计信息)。

Umoo — IoT Device Management Platform