Celium 架构

Celium 是一个自托管的 mesh 组网软件,功能对齐 Tailscale:给每台机器一个 虚拟 IP,让它们在任意网络环境下互联,走点对点加密隧道,打不通时自动回退到 中继,并且由一套中心化的策略(ACL / DNS / 密钥)统一下发。

        ┌──────────────────────────────────────────────┐
        │           协调服务器 control plane           │
        │  机器鉴权 · 节点注册 · 虚拟 IP 分配 · ACL     │
        │  DNS 配置 · DERP 拓扑 · netmap 长轮询推送     │
        └───────┬──────────────────────────────┬───────┘
                │ Noise 加密控制通道            │ 只交换公钥与策略,
                │ (HTTPS + Upgrade)            │ 永远看不到业务流量
     ┌──────────┴─────────┐          ┌─────────┴──────────┐
     │  节点 A  celiumd   │          │  节点 B  celiumd   │
     │  ┌──────────────┐  │ 直连 UDP │  ┌──────────────┐  │
     │  │ WireGuard 设备│◄─┼──────────┼─►│ WireGuard 设备│  │
     │  └──────┬───────┘  │  打洞成功 │  └──────┬───────┘  │
     │         │          │          │         │          │
     │   ┌─────┴─────┐    │  ┌───────┴─────┐    │          │
     │   │ magicsock │    │  │  magicsock  │    │          │
     │   └─────┬─────┘    │  └───────┬─────┘    │          │
     │    TUN / netstack  │          │          │          │
     └────────────────────┘          └─────────────────────┘
                │                            │
                └──────────► DERP 中继 ◄─────┘
                   打洞失败时的兜底路径,
                   中继只转发密文,无法解密

1. 设计原则

  1. 控制面与数据面彻底分离。 协调服务器只知道公钥、端点、主机名和策略,

没有任何能解密业务流量的密钥。它被攻破的后果是"策略被篡改",而不是 "流量被读取"。

  1. 节点状态是 netmap 的纯函数。 服务器下发完整的期望状态(谁在线、

什么密钥、走哪个中继、放行什么流量),节点不做增量猜测。这让重连、 策略变更、状态回滚都能用同一套逻辑处理。

  1. 身份分层,密钥轮换。 机器密钥(Ed25519)代表设备,长期存在;

节点密钥(Curve25519 = WireGuard 密钥)每次启动轮换,由控制面签名成 节点证书,所以对端能离线验证身份,不需要在建连时回查服务器。

  1. 一切都要在没有 root 的情况下可验证。 TUN 模式是默认交付形态,

但同一套代码有一个 userspace(gVisor netstack)后端,测试与 CI 用它 跑完整链路。这也是 Tailscale 的 --tun=userspace-networking

  1. 失败要能解释。 状态输出里必须能看到"这个 peer 为什么慢/不通":

当前走直连还是中继、最后一次握手、字节计数、NAT 类型。

2. 组件与代码结构

目录职责对应 Tailscale
types/key四类密钥(machine / node / disco / control)types/key
types/tailcfg控制面协议数据结构types/tailcfg
types/netmap运行时网络视图(带索引,不可变)types/netmap
types/ipnPrefs(期望状态)/ Status(观测状态)types/ipn
control/controlhttpNoise IK 握手 + HTTP Upgrade 隧道control/controlhttp
control/controlclient客户端状态机:注册、长轮询、应用 netmapcontrol/controlclient
controlplane协调服务器:注册表、IP 分配、ACL 编译、账号与邀请、管理 API闭源控制面
derpDERP 中继协议的服务端与客户端derp
disco打洞协议:ping/pong、端点发现、NAT 判定disco
wgengineWireGuard 设备与 peer 配置wgengine
wgengine/magicsock端点管理:直连优先、中继兜底(实现 conn.Bindwgengine/magicsock
net/tun真实 TUN 设备(macOS utun / Linux tun)与路由配置wgengine/router + tun
net/netstack无 root 的 userspace 数据面(gVisor)net/netstack
net/dnsMagicDNS 解析器与宿主 DNS 配置net/dns + net/dnscache
net/packetACL 数据包过滤器与 IP 头解析wgengine/filter
net/socks5userspace 模式的出口代理
ipn/localLocalBackend 状态机ipn/localapi + ipn/ipnlocal
ipn/ipnserver本地 API(unix socket 上的 HTTP)ipn/ipnserver
ipn/state机器密钥 / 节点密钥 / 偏好持久化ipn/store
engine数据面迁移到 Tailscale 实现的验证与结论
tstest端到端测试:真实控制面 + 真实中继 + 真实节点tstest

关于「对应 Tailscale」这一列:数据面(WireGuard 设备、打洞、中继、用户态协议栈) 最终直接使用 tailscale.com 的 Go 实现,不再自行重写;控制面与账号体系是 Celium 自己的。迁移的验证、类型翻译表、以及哪些包会被删除,记录在 docs/ENGINE-ADOPTION.mdengine/ 中。在这张表里保留对照, 是为了说明每个模块在架构上对应什么,而不是承诺两份实现长期共存。

cli / cmd/celium命令行cmd/tailscale
cmd/celiumd守护进程cmd/tailscaled
cmd/celium-control协调服务器进程

3. 身份与密钥

四种密钥,四种用途,Go 类型不同,编译器保证不会混用:

类型算法生命周期用途
key.MachinePrivate/PublicEd25519设备终身设备身份;控制面用它鉴权;Noise 静态密钥由它派生
key.NodePrivate/PublicCurve25519每次 up 轮换WireGuard 密钥;控制面签名成节点证书
key.DiscoPrivate/PublicCurve25519每次进程启动打洞消息的认证与加密
key.ControlPrivate/PublicEd25519服务器终身签名 netmap 与节点密钥

文本形式带前缀,mkey: / nodekey: / discokey: / controlkey:, 避免把密钥贴错字段——这是 mesh VPN 运维中最常见的一类事故。

机器密钥 → Noise 静态密钥:控制通道用 Noise IK,客户端静态密钥不是 另生成一把,而是从机器密钥确定性地派生(SHA-512(seed) 截断并 clamp, 公钥侧走 Edwards→Montgomery 双有理映射)。这样 Noise 握手本身就完成了 机器鉴权,控制面不需要额外一轮签名校验。

节点密钥 → 节点证书:控制面用自己的 control 私钥对节点公钥签名, 签名随 netmap 一起下发。于是任何节点都能离线验证"这个 WireGuard 公钥 确实属于该节点",中继和服务器都无法冒充。

4. 控制协议

传输:HTTPS 上的 HTTP/1.1 Upgrade。客户端向控制面发起

POST /machine/control HTTP/1.1
Upgrade: celium-control-protocol
Connection: Upgrade

服务端回 101 Switching Protocols,之后这条连接变成 Noise IK 加密的 字节流,上面再跑 HTTP(net/http 服务端与客户端各自套一层)。

握手参数与 Tailscale 相同: Noise_IK_25519_ChaChaPoly_BLAKE2s,prologue = celium-control-protocol。 IK 模式让客户端在第一条消息里就加密了自己的静态公钥,同时服务端静态 公钥在客户端是 pin 死的(--control-key),因此中间人无处可藏。

加密流上的接口:

端点方向说明
POST /machine/register请求/响应RegisterRequestRegisterResponse,节点加入 tailnet、拿到 node ID 与虚拟 IP
POST /machine/map长轮询流请求 MapRequest,响应是连续的 JSON 对象流MapResponse),连接保持打开,服务器有变化就推
POST /machine/logout请求/响应注销节点

netmap 推送语义:首个响应是完整快照(NetmapComplete=true),之后是 增量(PeersChanged / PeersRemoved)。节点端只需要实现"把 MapResponse 折叠进当前 NetworkMap",重连就是把折叠器重置。

端点上报(endpoint)走同一个 map 请求:节点每次探测出新的 UDP 端点, 或者 NAT 映射变化,就带着新的 endpoint 列表重开一次 map 请求,服务器更新 注册表并推给相关 peer。

5. 数据面

WireGuard 由 golang.zx2c4.com/wireguard(wireguard-go)提供——用的是 上游实现,不是重写协议,因为密码学协议自己实现是风险不是优势。

   IP 包 → TUN/netstack → wireguard-go device → magicsock.Bind → UDP/DERP

magicsock 实现 wireguard-go 的 conn.Bind 接口,因此它位于 WireGuard 与网络之间,可以自由选择"这个包从哪条路出去":

  • 每个 peer 维护一组候选端点:局域网地址、STUN 发现到的公网地址、

UPnP/NAT-PMP 映射地址、通过 peer 的本地 API 学到的地址。

  • 直连可用就用直连;握手失败或超时就切到该 peer 的 home DERP 中继。
  • 一旦直连恢复,立刻切回直连。切换对 WireGuard 透明:Bind 层换掉的是

发送路径,密钥与序号不变。

两个后端:

  • TUN(默认,需要 root):macOS 用 utun(syscall 打开

AF_SYSTEM/SYSPROTO_CONTROLcom.apple.net.utun_control), Linux 用 /dev/net/tun。配合路由表把 100.64.0.0/10(以及 subnet router / exit node 的 CIDR)指进去。

  • netstack(无 root):gVisor 用户态 TCP/IP 栈,进程内直接收发 IP 包,

对外只暴露一个 SOCKS5 代理,把本进程发起的连接接进 mesh。功能等价,但 只覆盖自己发起的连接,用于测试、容器与无特权环境。

虚拟 IP 从 100.64.0.0/10(CGNAT 段)分配,与 Tailscale 同段,好处是 不会与常见的家用/企业网段冲突。

6. disco:打洞与端点发现

UDP 上的轻量协议,消息格式:

magic[6]="CELIUM"  senderDiscoPub[32]  receiverDiscoPub[32]  nonce[24]  box(...)

载荷用 disco 共享密钥(X25519(disco 私钥, 对端 disco 公钥))做 NaCl box 加密,因此中继即使转发也无法伪造或篡改端点信息。

消息类型:

类型作用
ping探测对端在本机看到的源地址,同时测量 RTT;携带 txid 与"请从哪个地址回"
pong回显 txid,并报告自己看到的源地址
call-me-maybe让对端主动向我的一组候选地址发 ping,用于双方都在 NAT 后时同时开洞
call-me-maybe-endpoint端点变化时通知

NAT 判定(netcheck):向 DERP 节点的 STUN 端口发 STUN 请求,比较不同 目标下观察到的映射地址:

  • 同一端口 → 易打洞(endpoint-independent mapping)
  • 端口随目标变化 → 硬 NAT(symmetric),只能靠中继

NAT 类型直接影响策略:两个硬 NAT 之间的节点不会浪费时间反复打洞, 直接走 DERP。

7. DERP:中继兜底

DERP 是"最后手段"路径,不是 VPN 服务器:它转发的是 WireGuard 密文, 无法解密,也不知道里面是什么协议。

  • 客户端通过 wss://(测试时 ws://)连接,Upgrade: DERP
  • 帧格式:magic[4]="DERP" + type[1] + len[4] + payload。

类型包括 SendPacketRecvPacketKeepAliveNotePreferredPing/PongHealthPeerGoneRestarting

  • 每个节点在 netmap 里有一个 HomeDERP 区域;控制面按区域延迟指派。
  • DERP 之间也互相转发(mesh),因此两个 home 区域不同的节点也能通信。

加密边界要说清楚:客户端与中继之间由 TLS 保护;中继转发的载荷是 WireGuard 密文。因此中继能看到"谁在和谁通信"(这是它完成工作所必需的), 但看不到内容——解密它需要 WireGuard 私钥,而那把密钥只在两端存在。 DERP 协议本身不做端到端加密,因为它运载的东西已经是端到端加密的。

8. MagicDNS

每个节点在 100.100.100.100 上跑一个解析器(Tailscale 同名地址), 把 <host><host>.<tailnet>.celium. 解析为 peer 的虚拟 IP。 控制面通过 DNSConfig 决定:

  • 哪些后缀走 MagicDNS(split DNS),其它后缀透传给原宿主解析器;
  • 是否接管宿主 DNS 配置(CorpDNS);
  • 额外的静态记录与 DoH resolver。

名字映射规则:<short-host> 唯一时直接可用;重名时按用户/设备模型 消歧(Cellium 里简化为:重名节点只保留 FQDN 可用,并在状态里给出告警)。

9. ACL

策略用 JSON 描述,在控制面编译tailcfg.PacketFilter 下发:

{
  "groups":  { "group:dev": ["alice@example.com"] },
  "hosts":   { "db": "100.64.0.5" },
  "acls": [
    { "action": "accept", "src": ["group:dev"], "dst": ["db:5432"] },
    { "action": "accept", "src": ["tag:server"], "dst": ["tag:server:*"] }
  ],
  "tagOwners": { "tag:server": ["alice@example.com"] }
}

编译时把 group / host / tag 展开成具体前缀,得到一张按源地址排序的规则表。 节点端 net/packet 对每个出站与入站包做匹配:源必须是某个 peer 的虚拟 地址(防伪造),目的端口必须在放行范围内。默认拒绝。

10. 本地后端与 CLI

ipn/local 是一个状态机:

Stopped ──up──► Starting ──netmap 到达──► Running
                   │                        ▲
                   └──需要登录──► NeedsLogin ┘

CLI 不直接碰网络与密钥,它通过 unix socket 上的本地 HTTP API 与 celiumd 通信(ipn/ipnserver)。这样只有守护进程需要特权,CLI 本身 无权限要求,也避免了两份状态。

主要命令:

celium up [--advertise-routes=...] [--advertise-exit-node] [--accept-routes]
celium down
celium status [--json] [--peers]
celium ping <peer>
celium netcheck
celium login / logout
celium serve / celium derper / celium control(自托管服务端)

11. 威胁模型

对手能做到做不到
好奇的网络中间人看到加密流量解密、冒充节点(Noise + WireGuard 密钥都在端上)
被攻破的 DERP 中继拒绝服务、记录通信关系读取内容、伪造端点信息(disco 端到端认证)
被攻破的协调服务器篡改 ACL、下发错误 netmap、拒绝服务解密历史流量、冒充某节点与 peer 握手(节点证书要控制密钥签名,且客户端 pin 控制公钥)
拿到磁盘的本地攻击者读到机器密钥与节点私钥解密历史流量(WireGuard 前向保密)

12. 验证策略

  • 单元测试:密钥编解码、netmap 索引与最长前缀匹配、ACL 编译与匹配、

netmap 增量折叠、disco 消息编解码。

  • 端到端:一条 go test 起一个真实协调服务器 + 2~3 个真实节点

(netstack 后端),验证

  1. 节点拿到虚拟 IP 并互相可见;
  2. 经中继(DERP)能 ping 通;
  3. 直连 UDP 打通后自动切换到直连;
  4. ACL 拒绝未授权端口、放行授权端口;
  5. MagicDNS 名字解析到正确虚拟 IP;
  6. 中继断开后仍能直连。
  7. 系统级:真实 TUN 模式需要 root,脚本化在具备权限的机器上跑。