Skip to content

WireGuard 插件 ​

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


目录 ​

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

架构概览 ​

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

┌────────────────────────────────────────────────────────────────────────┐
│  后端(weave server)                                                    │
│                                                                        │
│  ┌──────────────────────┐     ┌───────────────────────────────────┐   │
│  │  WireGuardService    │     │  AdminService RPCs                │   │
│  │  ─────────────────── │     │  ─────────────────────────────── │   │
│  │  • CreateMeshNetwork │◄────│  CreateWireGuardNetwork           │   │
│  │  • IP 分配            │     │  EnrollGroupInNetwork             │   │
│  │  • 节点注册           │     │  AddDeviceToNetwork               │   │
│  │  • 配置构建           │     │  SetPeerRole / AssignRelay        │   │
│  │  • 配置分发           │     │  ListWireGuardPeers               │   │
│  └──────────┬───────────┘     └───────────────────────────────────┘   │
│             │ NATS 总线                                                │
└─────────────┼──────────────────────────────────────────────────────────┘
              │
    ╔═════════╧═════════╗
    ║    消息总线         ║
    ║  (NATS / local)    ║
    ╚═════════╤═════════╝
              │
┌─────────────┼──────────────────────────────────────────────────────────┐
│  设备代理(weave-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 地址分配 ​

IPAllocator(internal/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 接口(wg0、wg1、……)。接口名称从进程内计数器顺序分配——第一个网络为 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

配置分发 ​

DistributeToDevice 和 DistributeToNetwork 是两个分发入口点。

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/24 的 AllowedIPs 条目。
  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"}

SDK 层的 WireGuardPlugin.Start() 订阅 evt.plugin.wireguard.pubkey 并路由到 HandlePubkeyReport;它是当前 WireGuard 事件命名空间唯一的后端消费者。endpoint 字段为可选——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_id、peer_pubkey。

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

Weave — IoT Device Management Platform