总结摘要
从 ZeroTier 迁移到 Headscale 的完整历程,包括选型分析、架构设计、TLS 证书配置、端口被占用的替代方案、阿里云 CGNAT 地址段冲突与 netfilter-mode 修复,以及多次踩坑的实战经验。
从 ZeroTier 迁移到 Headscale 的完整历程,踩坑记录、端口配置、TLS 证书、子网路由等实战经验。
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 我也用了一年多,功能上其实没问题,但有几个决定性的痛点促使我迁移:
真正的痛点
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> 的域名,零配置:
1
2
3
4
5
| # ZeroTier 的做法
$ ssh root@10.147.17.23 # 你得记住这个 IP
# Tailscale 的做法
$ ssh root@myserver.example.net # 自动解析,不用记 IP
|
3. Exit Node 支持原始且复杂
ZeroTier 要做"出口网关"需要在路由表上手动配置,而且不支持一键开关。Tailscale 的 Exit Node 是原生功能:tailscale set --exit-node=<node> 一键切换,手机上也能用。
4. ACL 策略不如 Tailscale 灵活
ZeroTier 的规则引擎是自研语法,学习成本高。Tailscale 的 ACL 是类 JSON 的声明式语法,配合 tagged nodes、autoApprovers 等机制,更直观:
1
2
3
4
5
| {
"acls": [
{"action": "accept", "src": ["tag:server"], "dst": ["tag:db:*"]}
]
}
|
5. 自建控制面成本高
ZeroTier 可以自建(通过 moons + roots),但需要同时维护多个组件,且官方文档对自建一直不太友好。Headscale 一个二进制/Docker 容器搞定全部。
部署架构
服务器
- 云服务器:公网 IP
xxx.xxx.xxx.xxx - 容器化部署:Docker
- 组件:
headscale/headscale v0.29.2 — 控制面核心headplane — Web 管理面板(可选,仅通过 Tailscale 私网访问)
网络拓扑
1
2
3
4
5
6
7
8
9
10
11
| 公网 (xxx.xxx.xxx.xxx)
│
├─ 10443/TCP ── headscale API + DERP 中继
├─ 3478/UDP ── STUN (NAT 穿透)
│
└─ Tailnet (100.80.0.0/16) ← 自定义子网,方便记忆
├─ 100.80.0.1 iStoreOS (家)
├─ 100.80.0.4 云服务器-A
└─ 100.80.0.5 云服务器-B
│
└─ 子网路由 192.168.1.0/24 → iStoreOS 家里的局域网
|
Docker Compose 部署
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| # docker-compose.yaml
services:
headscale:
image: headscale/headscale:latest
container_name: headscale
restart: unless-stopped
volumes:
- ./data/headscale:/var/lib/headscale
ports:
- "10443:10443" # HTTPS API + DERP 中继
- "3478:3478/udp" # STUN
command: serve
headplane:
image: ghcr.io/tale/headplane:latest
container_name: headplane
restart: unless-stopped
volumes:
- ./data/headplane:/etc/headplane
ports:
- "127.0.0.1:3000:3000" # 仅本地访问,或通过 Tailscale 私网
depends_on:
- headscale
|
⚠️ 官方不支持容器化部署(
headscale 官方声明
),但社区 Docker 镜像 headscale/headscale 维护良好,实测可用。
首次启动步骤
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| # 1. 创建目录结构
mkdir -p data/headscale/tls data/headplane
# 2. 编写 config.yaml(见第八节配置对比)并放到 data/headscale/config/
# 3. 放置 TLS 证书到 data/headscale/tls/
# 4. 启动
docker compose up -d
# 5. 创建用户
docker exec headscale headscale users create <username>
# 6. 生成 Headplane API key
docker exec headscale headscale apikeys create
# 7. 编写 data/headplane/config.yaml(见 Headplane 配置章节)
# 8. 重启 headplane
docker compose restart headplane
|
节点加入
1
2
3
4
5
6
7
8
9
10
11
12
13
| # Linux 客户端
tailscale up --login-server=https://headscale.example.com:10443
# 在 headscale 上注册(通过 Headplane 面板或 CLI)
# 方式一:Headplane Web 面板 → Devices → Approve
# 方式二:CLI
docker exec headscale headscale nodes list
docker exec headscale headscale nodes register --user <username> --key <nodekey>
# 使用预授权密钥(推荐,无需手动批准)
docker exec headscale headscale preauthkeys create --user <username> --reusable
# 客户端:
tailscale up --login-server=https://headscale.example.com:10443 --auth-key=<key>
|
Headplane 管理面板
Headplane 是 Headscale 的第三方 Web UI。这里记录关键的配置和踩坑。
配置
1
2
3
4
5
6
7
8
9
10
11
| # data/headplane/config.yaml
headscale:
url: http://headscale:8080 # 容器内通过 Docker 网络访问
api_key: <your-api-key> # 由 headscale apikeys create 生成
server:
host: 0.0.0.0
port: 3000
cookie_secret: <random-32-char> # 随机字符串,openssl rand -hex 16
disable_api_key_login: false # 允许 API key 登录
|
生成 API Key
1
2
| docker exec headscale headscale apikeys create
# 输出类似: hskey-api-PR3zh3Os-Bm7-...
|
踩坑:空白页面
现象:访问 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
:41641 to *:* — WireGuard 直连 - UDP to
*:3478 — STUN(NAT 类型检测)
我的实际端口放行
| 端口 | 协议 | 公网可达 | 说明 |
|---|
| 10443 | TCP | ✅ | headscale API + DERP 中继(非标准端口,见下文) |
| 3478 | UDP | ✅ | STUN |
| 41641 | UDP | ✅ | WireGuard 直连(客户端侧) |
| 9090 | TCP | ❌ | Metrics,仅 localhost 监听 |
验证 DERP 中继
1
2
3
4
5
6
7
8
9
10
11
| $ tailscale debug derp headscale
{
"Info": [
"Region 999 == \"headscale\"",
"Successfully established a DERP connection",
"STUN response: xxx.xxx.xxx.xxx:58024"
],
"Errors": [
"Error connecting over IPv6: no such host" # 无 IPv6,不影响
]
}
|
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),只要在配置里显式指定:
1
2
3
| # config.yaml
server_url: https://headscale.example.com:10443
listen_addr: 0.0.0.0:10443
|
影响:
- 客户端连接正常(server_url 显式指定了端口,客户端会跟随)
- 少数 Tailscale 客户端在 fast-reconnect 等场景下可能硬编码假定 443(参见
issue #2164
),但目前 v0.29 实测未发现问题
- DERP 中继正常(DERP map 会使用 server_url 的端口)
方案二:Nginx SNI 分流
如果 443 上跑着多个 SNI 域名的 HTTPS 服务,可以用 Nginx 的 stream 模块做四层 SNI 分流:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| # nginx stream 模块
stream {
map $ssl_preread_server_name $backend {
headscale.example.com headscale_backend;
www.example.com website_backend;
default website_backend;
}
upstream headscale_backend {
server 127.0.0.1:10443;
}
upstream website_backend {
server 127.0.0.1:8443;
}
server {
listen 443;
proxy_pass $backend;
ssl_preread on;
}
}
|
⚠️ 注意:这是四层 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 做反代,配置示例:
1
2
3
4
5
6
| headscale.example.com {
reverse_proxy 127.0.0.1:8080 {
header_up True-Client-IP {remote_host}
header_up X-Real-IP {remote_host}
}
}
|
headscale 侧配置:
1
2
3
4
| server_url: https://headscale.example.com
listen_addr: 127.0.0.1:8080 # 仅本地监听,TLS 由 Caddy 处理
tls_cert_path: ""
tls_key_path: ""
|
结论
443 被占用时,用非标准端口(如 10443)直连是最简单可靠的方案,功能无影响。SNI 四层分流是更优雅的方案但需要额外配置。反向代理可行但官方不保证兼容性。
TLS 证书配置:非 443 端口如何设置
方式一:手动指定证书(推荐)
如果已经有证书文件(如从阿里云/腾讯云申请的免费证书,或者 Let’s Encrypt 手动签发的),直接配置:
1
2
3
| # config.yaml
tls_cert_path: /var/lib/headscale/tls/cert.pem # 证书文件(含完整证书链)
tls_key_path: /var/lib/headscale/tls/cert.key # 私钥文件
|
⚠️ 重要:根据
headscale TLS 文档
:
“The certificate should contain the full chain, else some clients, like the Tailscale Android client, will reject it.”
证书必须包含完整证书链(fullchain.pem),否则 Android 客户端会拒绝连接。
如果使用 Docker 部署,需要将证书挂载进容器:
1
2
3
4
5
6
| # docker-compose.yaml
services:
headscale:
volumes:
- ./tls/cert.pem:/var/lib/headscale/tls/cert.pem:ro
- ./tls/cert.key:/var/lib/headscale/tls/cert.key:ro
|
方式二:Let’s Encrypt 自动签发
Headscale 内置了 ACME 客户端,支持自动签发和续期:
1
2
3
4
5
| # config.yaml
tls_letsencrypt_hostname: "headscale.example.com"
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
tls_letsencrypt_challenge_type: HTTP-01
tls_letsencrypt_listen: ":http"
|
⚠️ 注意:
- 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 分钟随机触发
- 续期成功/跳过时不产生日志
手动验证证书到期时间:
1
2
3
4
5
6
| $ openssl s_client -servername headscale.example.com \
-connect headscale.example.com:10443 2>/dev/null \
| openssl x509 -noout -dates
notBefore=Feb 8 09:48:26 2026 GMT
notAfter=May 8 09:48:25 2026 GMT
|
如果是手动证书(方式一),建议用 cron 定时任务替换证书并重启 headscale:
1
2
3
4
5
6
7
8
9
10
11
| # 每月 1 号凌晨 3 点更新证书
# crontab -e
0 3 1 * * /usr/local/bin/renew-headscale-cert.sh
# renew-headscale-cert.sh
#!/bin/bash
# 将新证书拷贝到 headscale 的证书目录
cp /path/to/new/fullchain.pem /opt/headscale/tls/cert.pem
cp /path/to/new/privkey.pem /opt/headscale/tls/cert.key
# 重启 headscale 容器
docker restart 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 the tailscale0 interface 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:
1
2
| tailscale set --netfilter-mode=nodivert
tailscale up --login-server=https://headscale.example.com:10443
|
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:
1
2
3
4
| # config.yaml
prefixes:
v4: 100.80.0.0/16 # 方便记忆
v6: fd7a:115c:a1e0::/48
|
这纯粹是为了方便记忆——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 官方示例
的差异:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
| # ============================================================
# ① server_url — 客户端连接地址
# 官方默认: http://127.0.0.1:8080
# 我的配置: https://headscale.example.com:10443
# 差异: 必须配置为公网可访问的 HTTPS 地址
# ============================================================
server_url: https://headscale.example.com:10443
# ============================================================
# ② listen_addr — 监听地址
# 官方默认: 127.0.0.1:8080
# 官方生产: 0.0.0.0:8080
# 我的配置: 0.0.0.0:10443
# 差异: 直接监听 10443 提供 HTTPS(非标准端口),不经过反代
# ============================================================
listen_addr: 0.0.0.0:10443
# ============================================================
# ③ metrics_listen_addr — 不变
# 官方默认: 127.0.0.1:9090
# 我的配置: 127.0.0.1:9090 (一致)
# ============================================================
metrics_listen_addr: 127.0.0.1:9090
# ============================================================
# ④ prefixes — IP 地址池
# 官方默认: v4: 100.64.0.0/10
# 我的配置: v4: 100.80.0.0/16
# 差异: 缩小到 /16 子网,方便记忆
# 备注: 官方支持缩小范围,但不能超出 CGNAT/ULA 范围
# ============================================================
prefixes:
v4: 100.80.0.0/16 # 官方是 100.64.0.0/10
v6: fd7a:115c:a1e0::/48
allocation: sequential
# ============================================================
# ⑤ derp.server — 内嵌 DERP
# 官方默认: enabled: false
# 我的配置: enabled: true
# 差异: 启用自建 DERP 中继,减少对 Tailscale 官方 DERP 的依赖
# 注意: 启用后必须配置 stun_listen_addr 和 ipv4
# ============================================================
derp:
server:
enabled: true # 官方默认是 false
region_id: 999
region_code: "headscale"
region_name: "Headscale Embedded DERP"
stun_listen_addr: "0.0.0.0:3478"
automatically_add_embedded_derp_region: true
ipv4: xxx.xxx.xxx.xxx # 填写你的公网 IP
private_key_path: /var/lib/headscale/derp_server_private.key
# ============================================================
# ⑥ TLS 证书
# 官方默认: tls_cert_path: "" (不使用 TLS)
# 我的配置: 手动指定证书路径
# 差异: 自签/购买证书,而非 Let's Encrypt
# ============================================================
tls_cert_path: /var/lib/headscale/tls/cert.pem
tls_key_path: /var/lib/headscale/tls/cert.key
# ============================================================
# ⑦ DNS — MagicDNS
# 官方默认: base_domain: example.com
# 我的配置: 自定义域名
# 差异: 使用自己的域名,同时配置国内 DNS 上游
# ============================================================
dns:
magic_dns: true
base_domain: example.net # 官方示例是 example.com
nameservers:
global:
- 223.5.5.5 # 官方示例是 1.1.1.1,国内用阿里 DNS 更快
# ============================================================
# ⑧ node.expiry — 节点过期
# 官方默认: 0 (永不过期)
# 我的配置: 0 (一致)
# 备注: Tailscale SaaS 默认 180 天,如果你想要类似行为,设为 180d
# ============================================================
node:
expiry: 0
|
关键差异总结
| 配置项 | 官方默认 | 我的配置 | 原因 |
|---|
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 通常是服务器,不应被远程节点自动修改路由表。
修复:
1
2
3
4
5
| # 推荐方式:tailscale set 持久化(v1.46+)
tailscale set --accept-routes
# 或者 tailscale up 时带参数
tailscale up --login-server=https://headscale.example.com:10443 --accept-routes
|
坑 2:tailscale up 报错 “requires mentioning all non-default flags”
现象:
1
2
3
| $ tailscale up --login-server=https://headscale.example.com:10443 --accept-routes
Error: changing settings via 'tailscale up' requires mentioning all
non-default flags. Use --reset or explicitly mention all non-default settings.
|
原因:之前设置过其他非默认参数(如 --netfilter-mode=nodivert),tailscale up 要求每次列出所有非默认参数。
修复:用 tailscale set 替代 tailscale up 带参数:
1
2
| tailscale set --accept-routes --netfilter-mode=nodivert
tailscale up --login-server=https://headscale.example.com:10443
|
tailscale set 是官方推荐的方式——偏好设置和连接操作分离,配置持久化。
坑 3:路由在 table 52 而不是 main 表
现象:ip route show 看不到 Tailscale 路由,但 ping 能通。
原因:Tailscale 使用自定义路由表 52(而非 main 表),避免和系统路由冲突。
1
2
3
4
5
6
7
| # 错误
$ ip route show | grep 192.168.1
# (空)
# 正确
$ ip route show table 52
192.168.1.0/24 dev tailscale0 # ✅ 在这里
|
坑 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 在南京,家里也在南京——流量绕了成都再回来,延迟自然高。
诊断:
1
2
3
4
5
6
7
| # 查看当前连接状态
tailscale status
# 如果显示 "relay" 而非 "direct",说明在走 DERP 中继
# 检查是否直连
tailscale ping <node>
# 输出会显示是 direct 还是 relay
|
优化:
- 确保两端 UDP 41641 不被防火墙拦截
- 确保两端 STUN (UDP 3478) 可达
- 如果始终无法直连,考虑在离用户更近的地理位置部署 DERP 服务器
坑 7:server_url 和 base_domain 不能同域
现象:headscale 容器反复重启,日志报错:
1
2
| Error: initializing: loading configuration: server_url cannot use the same domain
as base_domain in a way that could make the DERP and headscale server unreachable
|
原因:server_url(如 https://headscale.example.com:10443)和 base_domain(如 example.com)如果使用相同的根域名,DERP 和 API 的路由可能冲突。
修复:base_domain 必须和 server_url 的域名不同。例如:
1
2
3
4
| server_url: https://headscale.example.com:10443
# ...
dns:
base_domain: example.net # 不同于 server_url 的 example.com
|
坑 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 都可以),关键是路径正确且证书包含完整证书链。
验证:
1
2
3
4
5
6
7
| # 确保证书包含完整链
openssl verify -CAfile /path/to/cert.pem /path/to/cert.pem
# 检查证书和私钥是否匹配
openssl x509 -noout -modulus -in /path/to/cert.pem | md5sum
openssl rsa -noout -modulus -in /path/to/cert.key | md5sum
# 两个 md5 值应该相同
|
坑 9:headscale 命令不能直接执行
现象:在宿主机上执行 headscale ... 命令报错 command not found。
原因:headscale 跑在 Docker 容器里,二进制 headscale 不在宿主机 PATH 中。
修复:所有管理命令通过 docker exec 执行:
1
2
3
4
5
6
| # 正确
docker exec headscale headscale users list
docker exec headscale headscale nodes list
# 错误
headscale users list # command not found
|
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 延迟可控) |
常用命令速查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| # === Headscale 管理 ===
docker exec headscale headscale nodes list # 列出所有节点
docker exec headscale headscale nodes list-routes # 列出所有路由
docker exec headscale headscale nodes approve-routes -i <ID> -r <CIDR> # 批准路由
docker exec headscale headscale users list # 列出用户
docker exec headscale headscale preauthkeys create -u <user> --reusable # 创建预授权密钥
# === 客户端 ===
tailscale status # 查看连接状态
tailscale set --accept-routes # 持久化接受路由
tailscale set --exit-node=<node> # 设置出口节点
tailscale debug derp-map # 查看 DERP 地图
tailscale debug derp headscale # 测试自建 DERP 连通性
tailscale ping <node> # 测试节点直连延迟
ip route show table 52 # 查看 Tailscale 路由表
# === 端口检测 ===
nc -zv <IP> 10443 # 检查 TCP 端口
nc -zuv <IP> 3478 # 检查 UDP 端口
# === 证书检查 ===
openssl s_client -servername headscale.example.com \
-connect headscale.example.com:10443 2>/dev/null \
| openssl x509 -noout -dates # 检查证书到期时间
|
参考资料
本文记录了从 ZeroTier 迁移到 Headscale 的完整历程,包括选型分析、架构设计、TLS 证书配置、端口被占用的替代方案、阿里云 CGNAT 地址段冲突与 --netfilter-mode 修复、以及多次踩坑的修复经验。如果你也在做类似的迁移,希望这些经验能帮到你。