Tailscale vs ZeroTier:内网组网选型与实战
Tailscale/Headscale vs ZeroTier:我的组网方案选型与实战
从 ZeroTier 迁移到 Headscale 的完整历程,踩坑记录、端口配置、TLS 证书、子网路由等实战经验。
为什么选择自建 Headscale 而不是 Tailscale 官方 SaaS
首先要弄清楚一个常见的误解:Tailscale 和 Headscale 不是两个竞品,Headscale 是 Tailscale 控制面的开源替代。
Tailscale 的问题
Tailscale 官方的 SaaS 模式确实方便——下载客户端、一键登录、自动组网。但用了几个月后,几个痛点逐渐暴露:
免费版限制 3 个用户:个人使用够,但一旦想给家人、朋友、其他设备分配独立账号,立刻就超了。付费版 $6/user/month,10 台设备就是 $60/月,性价比极低
控制面在别人手里:你的网络拓扑、ACL 策略、DNS 配置、所有节点的连接元数据,全部经过 Tailscale 的协调服务器。虽然 Tailscale 声称不看你流量(WireGuard 端到端加密),但元数据的归属权不是你
DERP 中继不可控:当 NAT 打洞失败时,流量会经过 Tailscale 官方的 DERP 中继服务器。中继节点遍布全球,但延迟不可控、带宽不保证,高峰时段可能很慢
没有数据主权:合规要求、数据审计、内网完全隔离——这些在 SaaS 模式下都做不到
为什么 Headscale 解决了这些问题
引用自 VPNSmith 的实践总结 :
“The promise is precise: 90% of Tailscale Premium’s value, for ~5% of the cash cost, with 100% data sovereignty.”
Headscale 提供 Tailscale 高级版 90% 的功能,大约 5% 的现金成本,以及 100% 的数据主权。
| 维度 | Tailscale SaaS | Headscale 自建 |
|---|---|---|
| 用户数限制 | 免费 3 人,$6/人/月 | 无限制 |
| 控制面归属 | Tailscale Inc. | 你自己 |
| 元数据隐私 | 经 Tailscale 服务器 | 完全本地 |
| DERP 中继 | 官方节点,延迟不可控 | 自建 DERP,延迟可控 |
| 数据主权 | 无 | 100% |
| 合规/审计 | 依赖第三方 | 完全自主 |
| 客户端 | 完全相同的 Tailscale 客户端 | 完全相同的 Tailscale 客户端 |
最重要的是:客户端用的一模一样。你的手机、电脑、服务器装的还是官方 Tailscale 客户端,只是把 --login-server 指向你自己的 Headscale 服务器。用户体验完全一致,但控制面 100% 归你。
为什么不是 ZeroTier
ZeroTier 我也用了一年多,功能上其实没问题,但有几个决定性的痛点促使我迁移:
真正的痛点
1. 协议栈太重,性能不如 WireGuard
ZeroTier 用的是自研协议(Layer 2 虚拟以太网),协议栈复杂,封装开销大。而 Tailscale 基于 WireGuard(Layer 3),是 Linux 内核主线支持的协议,极简极快:
WireGuard 核心代码不到 4000 行,而 ZeroTier 的核心协议实现超过 10 万行。简单意味着更少的 bug、更快的审计、更低的延迟。
实际测试中,同样的两台阿里云服务器之间,Tailscale 的吞吐量比 ZeroTier 高约 30-40%。
2. 没有 MagicDNS 等价物
ZeroTier 没有内置 DNS。你需要在每台机器上维护 /etc/hosts 或者自己搭 DNS 服务器。而 Tailscale 的 MagicDNS 让每个节点自动获得 <hostname>.<base_domain> 的域名,零配置:
3. Exit Node 支持原始且复杂
ZeroTier 要做"出口网关"需要在路由表上手动配置,而且不支持一键开关。Tailscale 的 Exit Node 是原生功能:tailscale set --exit-node=<node> 一键切换,手机上也能用。
4. ACL 策略不如 Tailscale 灵活
ZeroTier 的规则引擎是自研语法,学习成本高。Tailscale 的 ACL 是类 JSON 的声明式语法,配合 tagged nodes、autoApprovers 等机制,更直观:
5. 自建控制面成本高
ZeroTier 可以自建(通过 moons + roots),但需要同时维护多个组件,且官方文档对自建一直不太友好。Headscale 一个二进制/Docker 容器搞定全部。
部署架构
服务器
- 云服务器:公网 IP
xxx.xxx.xxx.xxx - 容器化部署:Docker
- 组件:
headscale/headscalev0.29.2 — 控制面核心headplane— Web 管理面板(可选,仅通过 Tailscale 私网访问)
网络拓扑
Docker Compose 部署
| |
⚠️ 官方不支持容器化部署(
headscale 官方声明
),但社区 Docker 镜像 headscale/headscale 维护良好,实测可用。
首次启动步骤
| |
节点加入
| |
Headplane 管理面板
Headplane 是 Headscale 的第三方 Web UI。这里记录关键的配置和踩坑。
配置
| |
生成 API Key
踩坑:空白页面
现象:访问 http://headscale.example.com:3000 显示空白,日志报错 <Router basename="/admin/"> is not able to match the URL "/"。
原因:Headplane 的 frontend 路由在 /admin/ 前缀下,直接访问根路径 / 会 404。
解决:访问 http://headscale.example.com:3000/admin/(注意末尾的 /admin/)。
踩坑:HTTPS Cookie 警告
现象:页面提示 “Headplane is configured to use secure cookies, but this site is being served over an insecure connection”。
原因:Headplane 配置了 cookie_domain 且启用了 secure cookie,但通过 HTTP(非 HTTPS)访问。
解决:
- 如果通过 Tailscale 私网 IP 访问,去掉
cookie_domain配置 - 或者通过 Nginx 反代 Headplane 并配置 HTTPS
端口放行:官方文档怎么说
Headscale 官方端口要求
根据 headscale 官方 requirements 文档 :
| 端口 | 协议 | 公开 | 用途 |
|---|---|---|---|
| 443 | TCP | ✅ | HTTPS 控制面 + DERP 中继(推荐) |
| 3478 | UDP | ✅ | STUN(嵌入式 DERP 服务器必需) |
| 80 | TCP | 可选 | Let’s Encrypt HTTP-01 验证(仅使用内置 ACME 时需要) |
| 9090 | TCP | ❌ | Metrics 端点(仅内网) |
Tailscale 官方文档也明确列出了 客户端需要的出站端口 :
- TCP to
*:443— 协调服务器 + DERP 中继- UDP from
:41641to*:*— WireGuard 直连- UDP to
*:3478— STUN(NAT 类型检测)
我的实际端口放行
| 端口 | 协议 | 公网可达 | 说明 |
|---|---|---|---|
| 10443 | TCP | ✅ | headscale API + DERP 中继(非标准端口,见下文) |
| 3478 | UDP | ✅ | STUN |
| 41641 | UDP | ✅ | WireGuard 直连(客户端侧) |
| 9090 | TCP | ❌ | Metrics,仅 localhost 监听 |
验证 DERP 中继
443 端口被占用怎么办?非标准端口的配置与影响
问题
根据 headscale 官方文档 :
“The Tailscale client assumes HTTPS on port 443 in certain situations. Serving headscale either via HTTP or via HTTPS on a port other than 443 is possible but sticking with HTTPS on port 443 is strongly recommended for production setups.” — 参见 issue #2164
但在实际部署中,443 端口经常被其他服务(如 Nginx、Caddy、网站)占用。占用时的解决方案:
方案一:使用非标准端口(我的选择)
如果 443 被 Nginx 等服务占用,可以用其他端口(如 10443),只要在配置里显式指定:
影响:
- 客户端连接正常(server_url 显式指定了端口,客户端会跟随)
- 少数 Tailscale 客户端在 fast-reconnect 等场景下可能硬编码假定 443(参见 issue #2164 ),但目前 v0.29 实测未发现问题
- DERP 中继正常(DERP map 会使用 server_url 的端口)
方案二:Nginx SNI 分流
如果 443 上跑着多个 SNI 域名的 HTTPS 服务,可以用 Nginx 的 stream 模块做四层 SNI 分流:
| |
⚠️ 注意:这是四层 TCP 代理(不做 TLS 终结),而非七层 HTTP 反向代理。headscale 的 WebSocket 协议(tailscale-control-protocol)和 STUN 都能正常工作。
方案三:反向代理(有局限性)
headscale 社区提供了一份 反向代理文档 ,但标注为:
⚠️ Community documentation — not actively maintained by headscale authors. It is not verified by headscale developers.
反向代理的限制:
- STUN (UDP/3478) 无法代理:必须直连暴露
- WebSocket 协议特殊:Tailscale 客户端用的是
tailscale-control-protocol(不是标准WebSocket),Upgrade头值不同,反代需要额外配置 - Cloudflare 不支持:Cloudflare 不支持 WebSocket POST 方式升级
如果用 Caddy 做反代,配置示例:
headscale 侧配置:
结论
443 被占用时,用非标准端口(如 10443)直连是最简单可靠的方案,功能无影响。SNI 四层分流是更优雅的方案但需要额外配置。反向代理可行但官方不保证兼容性。
TLS 证书配置:非 443 端口如何设置
方式一:手动指定证书(推荐)
如果已经有证书文件(如从阿里云/腾讯云申请的免费证书,或者 Let’s Encrypt 手动签发的),直接配置:
⚠️ 重要:根据 headscale TLS 文档 :
“The certificate should contain the full chain, else some clients, like the Tailscale Android client, will reject it.”
证书必须包含完整证书链(fullchain.pem),否则 Android 客户端会拒绝连接。
如果使用 Docker 部署,需要将证书挂载进容器:
方式二:Let’s Encrypt 自动签发
Headscale 内置了 ACME 客户端,支持自动签发和续期:
⚠️ 注意:
- HTTP-01 验证:Let’s Encrypt 会连接你的 80 端口验证域名所有权。如果你用非 443 端口(如 10443),
listen_addr是 10443,但 ACME 验证仍然需要 80 端口。如果 80 也被占用,可以改tls_letsencrypt_listen到其他端口,然后用防火墙将 80 流量转发过来 - TLS-ALPN-01 验证:如果 80 不可用,可以改用 TLS-ALPN-01 挑战。但 Let’s Encrypt 只会连接 443 端口做验证,如果
listen_addr不是 443,需要防火墙转发 443 到实际端口
证书续期
根据 headscale TLS 文档 :
- Let’s Encrypt 证书有效期 3 个月
- Headscale 在证书剩余 30 天以内时才会尝试续期
- 续期尝试间隔 30-60 分钟随机触发
- 续期成功/跳过时不产生日志
手动验证证书到期时间:
如果是手动证书(方式一),建议用 cron 定时任务替换证书并重启 headscale:
| |
阿里云 CGNAT 地址段冲突:--netfilter-mode 的真正作用
Tailscale 使用 100.64.0.0/10(CGNAT 地址段,RFC6598)作为节点 IP 地址池。阿里云 ECS 在 100.100.x.x 段部署了内部服务(DNS 100.100.2.136/138、元数据 100.100.100.200 等)。
冲突机制
很多人以为冲突是因为 Tailscale 加了 100.64.0.0/10 dev tailscale0 路由,把流量劫持走了。但实际上,即使路由表里没有这条路由,冲突依然存在。
真正的根因在 Tailscale 的 --netfilter-mode。根据
Tailscale netfilter modes 官方文档
,默认 on 模式下,Tailscale 会在 iptables 里加一条规则:
Prevent CGNAT spoofing: Traffic from the CGNAT range (
100.64.0.0/10) that does not arrive through thetailscale0interface is dropped. This prevents attackers from spoofing Tailscale IP addresses on other interfaces.
翻译:凡是来源或目的 IP 落在 100.64.0.0/10 范围内、且不是从 tailscale0 接口进出的包,一律 DROP。
这条规则是为了安全——防止外部攻击者伪造 CGNAT 地址。但副作用是:阿里云 ECS 的内网 DNS(100.100.2.136)和元数据服务(100.100.100.200)也在这个范围内,它们的流量是从 eth0 进来的,不符合 “from tailscale0” 的条件,被 iptables 直接 DROP 了。
修复方案
将 --netfilter-mode 设为 nodivert:
nodivert 的含义:Tailscale 仍然创建 iptables 子链(规则还在),但不把它们挂载到主链的 INPUT/FORWARD 钩子上。这样 CGNAT 反欺骗规则被停用,但规则定义保留,你可以按需手动激活。
影响分析
--netfilter-mode=nodivert 关闭了 Tailscale 自动管理的 4 类 iptables 规则:
| 规则 | 作用 | 关闭后影响 |
|---|---|---|
| Accept tailnet traffic | 放行 tailscale0 接口流量 | 如系统默认策略是 ACCEPT,无影响 |
| Prevent CGNAT spoofing | DROP 非 tailscale0 的 100.64.0.0/10 包 | ⬅️ 这就是要关的,关了阿里云内网服务就通了 |
| Accept Tailscale protocol | 放行 UDP 41641 端口 | 如系统默认策略是 ACCEPT,无影响 |
| Enable routing features | 子网路由/exit node 的转发和 NAT | 需手动配置 iptables 转发规则 |
对 Headscale 控制面:完全不受影响。节点注册、密钥分发、ACL 同步走 HTTPS,和 netfilter 无关。
对 P2P 打洞:不受影响。打洞依赖的是 WireGuard 的 UDP 通信,只要系统默认 INPUT 策略是 ACCEPT(阿里云 ECS 默认是),UDP 41641 的入站包不会被挡。
对 DERP 中继:DERP 的 STUN(UDP 3478)和 HTTPS 中继端口,只要系统默认策略是 ACCEPT 就不受影响。
为什么选 nodivert 而不是 off
off 完全删除所有 Tailscale 的 iptables 规则,后续想恢复得重新执行 tailscale up。nodivert 保留了规则定义,只是不激活,方便以后按需启用。
缩小 IP 池(顺手做了)
我还把 IP 地址池从默认的 100.64.0.0/10 缩小到了 100.80.0.0/16:
这纯粹是为了方便记忆——100.80.0.x 比 100.64.x.x 好记。/16 的网段也足够个人使用。
⚠️ 官方提醒(来自 config-example.yaml ):
“Using a SUBSET of these ranges is supported and useful if you want to limit IP allocation to a smaller block. Using ranges OUTSIDE of CGNAT/ULA is NOT supported and will cause undefined behaviour.”
参考
配置对比:我的配置 vs 官方示例
以下是关键配置项的对比,标注了与 headscale v0.29.1 官方示例 的差异:
| |
关键差异总结
| 配置项 | 官方默认 | 我的配置 | 原因 |
|---|---|---|---|
server_url | http://127.0.0.1:8080 | https://...:10443 | 生产环境必须 HTTPS |
listen_addr | 127.0.0.1:8080 | 0.0.0.0:10443 | 443 被占用,使用非标准端口 |
prefixes.v4 | 100.64.0.0/10 | 100.80.0.0/16 | 方便记忆 |
derp.server.enabled | false | true | 自建 DERP,降低延迟 |
tls_cert_path | "" (空) | 手动指定 | 使用自己的证书 |
dns.nameservers | 1.1.1.1 | 223.5.5.5 | 国内阿里 DNS 延迟更低 |
踩坑记录
坑 1:Linux 客户端不会自动接受子网路由
现象:子网路由在 Headscale 端已批准,但 Linux 服务器 ping 不通子网内的 IP。
根因:Tailscale 在 Linux 上默认不启用 --accept-routes。这是 Tailscale 的设计行为,而非 bug:
| 平台 | 默认接受路由 |
|---|---|
| Windows / macOS / iOS / Android | ✅ |
| Linux | ❌ |
Linux 默认不接收路由的原因:安全考虑——Linux 通常是服务器,不应被远程节点自动修改路由表。
修复:
坑 2:tailscale up 报错 “requires mentioning all non-default flags”
现象:
原因:之前设置过其他非默认参数(如 --netfilter-mode=nodivert),tailscale up 要求每次列出所有非默认参数。
修复:用 tailscale set 替代 tailscale up 带参数:
tailscale set 是官方推荐的方式——偏好设置和连接操作分离,配置持久化。
坑 3:路由在 table 52 而不是 main 表
现象:ip route show 看不到 Tailscale 路由,但 ping 能通。
原因:Tailscale 使用自定义路由表 52(而非 main 表),避免和系统路由冲突。
坑 4:Docker 容器映射了不需要的端口
现象:headscale 的 Docker 镜像默认暴露了 8080 端口,但配置里 listen_addr 是其他端口,8080 成了死端口。
影响:虽然安全组可能阻断了 8080,但在主机上它是暴露的。如果安全组放行,就会有一个无用的端口暴露在公网。建议在 docker-compose.yaml 里只映射实际使用的端口。
坑 5:ZeroTier 和 Tailscale 共存冲突
现象:Windows 上启用 Tailscale 之后,无法访问 ZeroTier 的 192.168.191.0 网段。
原因:两个 VPN 客户端同时运行时,路由表可能互相覆盖。Tailscale 和 ZeroTier 在路由表、DNS、iptables 层面存在竞争。
修复:
- 不建议在同一台机器上同时运行两个 VPN 客户端
- 如果必须共存,用
tailscale set --exit-node-allow-lan-access配置 LAN 访问策略 - 临时方案:
tailscale down停掉 Tailscale 后再访问 ZeroTier 网段
坑 6:DERP 中继延迟高
现象:Windows 连到南京的家中节点,ping 延迟 120ms。
原因:当 NAT 打洞失败时,流量走 DERP 中继。DERP 服务器在阿里云成都,Windows 在南京,家里也在南京——流量绕了成都再回来,延迟自然高。
诊断:
优化:
- 确保两端 UDP 41641 不被防火墙拦截
- 确保两端 STUN (UDP 3478) 可达
- 如果始终无法直连,考虑在离用户更近的地理位置部署 DERP 服务器
坑 7:server_url 和 base_domain 不能同域
现象:headscale 容器反复重启,日志报错:
原因:server_url(如 https://headscale.example.com:10443)和 base_domain(如 example.com)如果使用相同的根域名,DERP 和 API 的路由可能冲突。
修复:base_domain 必须和 server_url 的域名不同。例如:
坑 8:TLS 证书文件名不影响功能
现象:headscale 日志提示 WRN listening without TLS but ServerURL does not start with http://,但证书文件明明存在。
原因:headscale 在配置中找到 tls_cert_path 和 tls_key_path 后才会启用 TLS。文件名无所谓(cert.pem、fullchain.pem 都可以),关键是路径正确且证书包含完整证书链。
验证:
坑 9:headscale 命令不能直接执行
现象:在宿主机上执行 headscale ... 命令报错 command not found。
原因:headscale 跑在 Docker 容器里,二进制 headscale 不在宿主机 PATH 中。
修复:所有管理命令通过 docker exec 执行:
ZeroTier vs Headscale:最终选择
| 场景 | 建议 |
|---|---|
| 个人使用、不想折腾 | Tailscale 官方 SaaS(免费 3 用户够用) |
| 超过 3 个用户、需要数据主权 | Headscale 自建 |
| 需要 MagicDNS(自动域名) | Headscale/Tailscale(ZeroTier 没有) |
| 需要 Exit Node 一键切换 | Headscale/Tailscale(ZeroTier 配置复杂) |
| 需要 Layer 2 虚拟以太网 | ZeroTier(Tailscale 只支持 Layer 3) |
| 大量 Linux 服务器组网 | Headscale(但要记得 --accept-routes) |
| 对延迟敏感、需要自控 DERP | Headscale(自建 DERP 延迟可控) |
常用命令速查
| |
参考资料
- Headscale 官方文档 — Requirements
- Headscale 官方文档 — TLS 配置
- Headscale 官方文档 — DERP 配置
- Headscale 配置示例 (v0.29.1)
- Tailscale 官方文档 — 防火墙端口
- Tailscale 官方文档 — CGNAT 冲突排查
- Headscale 反向代理文档 (社区维护)
- Headscale issue #2164 — 443 端口假设
- VPNSmith — Headscale 实战总结
本文记录了从 ZeroTier 迁移到 Headscale 的完整历程,包括选型分析、架构设计、TLS 证书配置、端口被占用的替代方案、阿里云 CGNAT 地址段冲突与 --netfilter-mode 修复、以及多次踩坑的修复经验。如果你也在做类似的迁移,希望这些经验能帮到你。