拆开 Tailscale 才发现,36K Star 的它只开源了一半
- 数据面(magicsock、DERP、路由)以 BSD-3 开源,负责撮合的控制面服务器闭源,付费业务建立在托管协调服务上
- magicsock 实现可在使用中切换通信路径的 socket,按直连、peer relay、DERP 三层降级,并通过心跳持续探测路径活性
- 协议兼容性靠单调递增的 CapabilityVersion 整数协商,tailcfg.go 注释完整记录五年半的协议演化史
- 9214 行的 ipnlocal 单体正通过 feature/ 目录与 ts_omit_ 构建标签渐进拆分,README 坦承迁移只完成一半
- 「撮合集中、传包分散」的架构切分可迁移到目录小流量、内容大流量的其他系统设计
大家好,我是若风。
你有没有过这种经历。人在咖啡馆,想连一下家里的 NAS,或者给跑在家里的那台小机器发个命令。ssh 超时,端口转发不知道指向哪,公网 IP 是不存在的,NAT 把你和自己的设备隔在两个世界。
这个场景催生了一票工具,Tailscale 是里面名气最大的那个。36213 个 Star,3182 个 Fork,Go 写的,2020 年 1 月建仓,发稿前几天还有提交。但拆进去我第一件事不是看代码,而是被它的 README 惊到了。
87 行。
3464 个文件的代码库,README 只有 87 行,几乎没有一行讲架构。它只在 Overview 里轻描淡写了一句,this repository contains the majority of Tailscale's open source code。majority,大部分。这个词选得很精确,精确到你可以把整篇文章的线索都押在它上面。
开源的一半,和不开源的另一半
Tailscale 解决的问题,官方描述是 the easiest, most secure way to use WireGuard。WireGuard 已经把「加密隧道」做到极致,Linux 内核原生支持,性能拉满。但 WireGuard 有个前提,你得手动配置 peer,手动交换公钥,手动分配 IP。两台都躲在 NAT 后面的机器,连「找到对方」都做不到。
Tailscale 的做法是把问题切成两半。
撮合这件事,谁在线、公钥是什么、ACL 允许谁访问谁,交给一台中心化的协调服务器,官方叫 control plane。传数据这件事,加密包从 A 到 B,不经过任何中心节点,能直连就直连。
关键来了。跑流量那一半,magicsock、DERP、路由、防火墙规则,全部在仓库里,BSD-3 协议。撮合那一半,协调服务器的代码不在仓库里,也不开源。
这不是藏着掖着。ipn/prefs.go 里有个常量叫 DefaultControlURL,值是 https://controlplane.tailscale.com。默认协调服务器就是 Tailscale 公司托管的那台,付费生意也建立在这上面。你在仓库里翻到的 control/ 目录,controlclient、controlhttp、ts2021,全是客户端一侧的代码,负责跟那台闭源服务器对话。
把仓库的分层摊开看,大概是这样的。
五层里数据面占了三层半,从 magicsock 到 DERP 一层不缺,而撮合一切的那台服务器,只能以虚线框的姿态站在图外。
这个切分在商业上极其聪明。数据面开源,加密和传输可以审计,企业安全团队放行。控制面闭源,身份管理、SSO、审计日志这些企业愿意付钱的能力留在自己手里。
社区用脚投了票。headscale,一个用 Go 重写的开源协调服务器,43628 个 Star。你没看错,重写闭源那一半的项目,Star 比官方客户端仓库还多七千多个。
一个会在半路换路的 socket
数据面的核心在 wgengine/magicsock/magicsock.go,4589 行。包注释只有一句话,implements a socket that can change its communication path while in use,一个能在使用过程中改变通信路径的 socket。
你想想看这句话有多反直觉。传统 socket 的哲学是,连接建立之后路径就定了。magicsock 的哲学是,路径是猜出来的,而且随时可以换。
它内部给每个节点维护一串候选路径,按优先级三层降级。
第一层,直连。两端各自向 STUN 服务器发探测包,先弄清自己的 NAT 类型。net/netcheck/netcheck.go 里的判定逻辑很直接,一个叫 MappingVariesByDestIP 的字段,如果对着不同 STUN 服务器探测出来的公网映射不一样,说明是端口随机型 NAT,打洞难度升级。探测超时卡在 3 秒,stunProbeTimeout,赌的就是 STUN 回包快。
第二层,peer relay。tailcfg.go 的 CapabilityVersion 120 注释记着,2025 年 7 月 15 日,client understands peer relay disco messages。打洞失败时,让 tailnet 里另一台有公网能力的节点当中转,流量不碰官方服务器。这是 2025 年才落地的能力,社区喊去中心化中继喊了很多年。
第三层,DERP。官方中继,兜底中的兜底。
三层降级画成图就是这个样子。
翻译一下这张图,直连是常态,DERP 是保险,中间的 peer relay 是 2025 年才补上的过渡档。
切换不是等出事才做。endpoint.go 里有一整套心跳机制,heartbeatForLifetime 调度 disco ping 持续确认路径活着,UDP 映射的寿命 cliff 也有专门探测。网络一变,比如你从 WiFi 切到 5G,它感知得到,把流量挪到新路径上。
还有个细节我特别喜欢。socketBufferSize = 7 << 20,7 MB 的 UDP 收发缓冲。注释写着,7 MB 是 macOS 默认配置支持的上限,其他平台会悄悄往下截。跨平台项目对每个平台的内核参数抠到这个程度,不多见。
协议头里焊着一把钥匙 emoji
第三层值得单独讲,因为它是我见过最有幽默感的协议设计。
DERP 全称 Designated Encrypted Relay for Packets。它不解析你的流量,流量本来就是 WireGuard 加密的,它只负责转发密文。寻址方式清奇,用 curve25519 公钥当地址,中继服务器不知道也不需要知道你在传什么。
然后是那个 magic number。
// Magic is the DERP Magic number, sent in the FrameServerKey frame
// upon initial connection.
const Magic = "DERP🔑" // 8 bytes: 0x44 45 52 50 f0 9f 94 91
一个 emoji 直接焊在协议头里,DERP 四个字母加钥匙 emoji 的 UTF-8 字节,抓包肉眼可辨。包体上限 64 KB,MaxPacketSize = 64 << 10。注释里明说,DERP is a last resort,最后手段。整个设计摆明了态度,能不用我尽量不用我,但你卡死的时候我永远在。
有意思的是,DERP 服务器本身开源,derp/derpserver 是完整实现,可以自建。更狠的是 derp/xdp 目录,里面躺着用 eBPF/XDP 写的 STUN 服务器,把包处理下沉到网卡驱动层,一台中继机器硬扛海量探测的工程答案。
把版本协商写成编年史
拆这个仓库最享受的部分,是 tailcfg/tailcfg.go 里那个 CapabilityVersion,一个 3329 行文件撑起的协议契约。
客户端和协调服务器怎么协商兼容性?Tailscale 没用 semver,也没用 feature flag 列表,就一个单调递增的整数。客户端在请求里带上自己的版本号,服务器看一眼就知道对面懂什么、不懂什么。
更狠的是那份注释。从版本 3 开始,每一条都记着日期和内容,一路记到现在的 145。随手摘几条,5 号 2020 年 10 月支持 MagicDNS,65 号 2023 年 7 月增量 DERPMap 更新,120 号 2025 年 7 月 peer relay,129 号 2025 年 10 月修掉 peer relay 在睡眠唤醒时的死锁。
一个整数字段,五年半的协议演化史。每一条语义变更都得想清楚老客户端怎么办,这份注释比大多数公司的架构文档都诚实。
9214 行的单体,和 80 个逃生舱标签
拆到这里都是漂亮话,接下来是不漂亮的部分。
ipn/ipnlocal/local.go,9214 行。LocalBackend,客户端的状态机中枢,登录、密钥、ACL、peer 状态、serve 配置,什么都往里堆。这是每个成功项目都会长出来的器官,功能越多它越大,它越大改起来越疼。
Tailscale 自己也疼。2025 年 9 月,仓库里出现了 feature/ 目录,第一刀在 9 月 12 日落下。方案是每个功能一个独立 Go 包,通过 hooks 和注册机制自己挂进主流程,构建时用 ts_omit_ 前缀的 build tag 把不要的功能裁掉。
动机写在 feature/README.md 开头,原文大意是,几美元芯片上的 IoT 设备不需要 Taildrop、WebDAV、ACME 和 SSH。登记处是 feature/featuretags/featuretags.go,单一事实来源,现在挂着 80 个特性标签,ssh、taildrop、acme、drive 都在册,标签之间还有依赖关系,组成一张 DAG。
坦白讲,最让我意外的不是方案,是那份 README 的坦率。它专门有一节标题就叫 The half-migrated reality,半迁移的现实。承认大量功能一半代码挪进了 feature/,另一半还留在 ipn/ipnlocal 和 cmd/tailscaled,靠构建标签和 if buildfeatures.HasFoo 兜着。还劝贡献者,碰到的时候顺手多迁一点,但不必一个 PR 迁完。
到今天,这个迁移跑了一年,local.go 还是 9214 行。大厂拆单体的节奏跟社区想象的不一样,不追求一步到位,接受长期共存。
顺带一提 feature 化的极致受益者,tsnet。它把整个 Tailscale 节点塞进你的 Go 进程,用 gVisor 的用户态 TCP/IP 栈,不要 root,不要系统守护进程,一个二进制里能跑多个独立节点。Server.Listen 返回标准库的 net.Listener,现成 HTTP 服务零改造接入 tailnet,嵌进 CLI 工具和边缘服务都很顺。
讨论了五年还没合上的那几个 issue
issue 区的考古结果同样诚实。
mDNS 支持,issue #1013,92 条讨论,至今 open。苹果生态的设备发现靠 mDNS,跨子网转发这个需求社区从 2020 年喊到现在,官方态度一直是想做但优先级排不上。
同时连多个 tailnet,issue #183,同样是 2020 年开的。一个身份一个 tailnet,这是架构层的决定,不是加个开关的事。要做运维顾问的人在多套账号间切来切去,讨论绵延五年没停。
移动端电池,issue #3363,76 条讨论还在收新的复现。NAT 保活和系统省电的战争没有终局。
这些不算丑闻。一个项目的 issue 区比它的 README 诚实得多,哪些是「快了快了」,哪些是「架构上就不打算支持」,时间线不会说谎。
横向再看一眼同类。ZeroTier,老牌对手,17082 Star,数据面同为 P2P,控制器可以自建,但仓库许可证已经换成 BSL 这类源码可用协议,不算标准开源。手工 WireGuard 性能最好,配置成本最高,NAT 后面基本抓瞎。headscale 配官方客户端,控制面自托管,数据面照用官方代码,是自托管圈的事实标准。Tailscale 官方托管,零配置体验的天花板,代价是撮合层必须信这家公司。
| 方案 | 控制面 | 数据面 | 适合谁 |
|---|---|---|---|
| Tailscale 官方 | 托管闭源 | 开源可审计 | 想省心的 |
| headscale + 官方客户端 | 自托管 | 同左 | 要自主的 |
| ZeroTier | 可自建但许可受限 | P2P | 老牌生态用户 |
| 手工 WireGuard | 不存在 | 内核态最快 | 固定拓扑玩家 |
撮合集中,传包分散
拆完这个仓库,我一直觉得最值得偷的不是哪段代码,是那个切分。
撮合集中,传包分散。中心化服务器只做低频轻量的活,密钥分发、节点目录、ACL 下发,这些流量每分钟几 KB。真正的大流量,视频、备份、git clone,全部 P2P 直连,中心服务器碰都碰不到。那台闭源服务器就算宕机,已经建立的连接照常工作,公司想作恶也拿不到你的流量。
这个模式离 VPN 很远也成立。做多人协作工具,状态同步走中心,文档传输 P2P。做 IoT 平台,设备目录走中心,固件分发走边缘。凡是「目录小流量、内容大流量」的系统,都值得把这道切分想一遍。
至于要不要用 Tailscale 本身,选型结论很简单。个人和团队想省心,直接官方。有合规要求或者就是不想依赖外部服务,headscale 顶上,客户端代码基本一行不用改。毕竟这个仓库里跑流量的那一半,本来就是你的。

评论互动