Headscale 极简部署指南
0. 架构与端口规划(先把端口定死)
| 服务 | 协议/端口 | 说明 | 谁访问谁 |
|---|---|---|---|
| HTTPS | TCP/443 |
Headscale 控制面 + DERP(经反代,含 Upgrade) | 公网 → Nginx/OpenResty |
| STUN | UDP/3478 |
内置 STUN(打洞关键) | 公网 → Headscale(直连到服务器) |
| Local | TCP/3477 |
Headscale 本体(仅本机监听) | Nginx/OpenResty → Headscale |
关键点:
3478必须放行 UDP(不是 TCP):很多云安全组 TCP/UDP 分开选,别选错。- 不要对公网暴露
3477:只监听127.0.0.1,由反代统一出入口。
1. 前置准备(替换占位符)
把下面占位符替换成你的值:
hs.example.com:你的控制面域名(必须 HTTPS 可访问)SERVER_PUBLIC_IP:服务器公网 IPv4REGION_CODE:自建 DERP code(建议小写,例如myderp)
准备项:
- 一台 Linux 服务器(有公网 IPv4)
- 一个域名 A 记录指向
SERVER_PUBLIC_IP - 已有证书(或你能自己搞定 ACME 申请;本文不展开)
- Nginx 或 OpenResty 已安装
2. 安装 Headscale(二进制)
从 juanfont/headscale 的 Releases 下载与你机器架构匹配的二进制:
安装到 /usr/local/bin/headscale:
1 | sudo cp ./headscale /usr/local/bin/headscale |
小坑:如果遇到
sudo headscale: command not found,多半是sudo的secure_path不含/usr/local/bin。本文后续统一用绝对路径:sudo /usr/local/bin/headscale ...
3. 生成必须密钥(v0.28+ 常见强制项)
创建目录:
1 | sudo mkdir -p /etc/headscale /var/lib/headscale |
生成 Noise 私钥:
1 | sudo /usr/local/bin/headscale generate private-key \ |
说明:这里用
tee是为了解决“sudo+ 重定向”权限问题(sudo cmd > file的重定向不在 sudo 权限里执行)。
另外,内容通常带privkey:前缀,别手动删,否则可能报 “expected type prefix privkey:”。
4. 写入 Headscale 配置(最小可用模板)
保存为:/etc/headscale/config.yaml
说明:先关闭 DNS/MagicDNS,减少必填项耦合;跑通后再按「进阶」章节开启。
1 | # /etc/headscale/config.yaml |
配置校验(必须过):
1 | sudo /usr/local/bin/headscale configtest -c /etc/headscale/config.yaml |
5. systemd 启动 Headscale
创建:/etc/systemd/system/headscale.service
1 | [Unit] |
启动并自启:
1 | sudo systemctl daemon-reload |
生产提示:为简单起见,上面未指定
User=/Group=,默认 root 运行。生产环境建议创建专用用户(如headscale)并收紧/var/lib/headscale权限后再以非 root 运行。
6. Nginx/OpenResty 反代(关键是 Upgrade + 长连接)
示例:/etc/nginx/conf.d/headscale.conf
1 | map $http_upgrade $connection_upgrade { |
检查并重载:
1 | sudo nginx -t |
7. 创建用户 + 客户端接入
先说明一下什么是“用户(user)”:
- Headscale 的 user 不是登录账号/密码体系,而是一个命名空间/分组:把一批设备(nodes)归到同一个用户下面,便于管理与发放入网 key。
- 个人/小团队最常见:只建 1 个 user,然后所有设备都加入这个 user。
tailnet-user只是示例名,你可以改成自己的(例如alice/team-a)。
创建用户:
1 | # 创建一个 user(示例名:tailnet-user) |
生成 preauth key(优先用用户名;旧版本不支持再用数字 ID):
1 | # 优先:直接用用户名 |
客户端加入(Linux 示例):
1 | sudo tailscale up \ |
后续新增设备:重复本节“生成 key → 客户端 tailscale up”即可。
8. 验收清单(所有检查统一在这里做)
8.1 服务器侧
1 | sudo /usr/local/bin/headscale configtest -c /etc/headscale/config.yaml |
看到 curl -i https://hs.example.com/derp 返回 426 且包含 DERP requires connection upgrade:正常(代表路由打通)。
8.2 客户端侧(最重要)
1 | tailscale status |
tailscale status 速记:
relay/via DERP(...):走 DERP 中继(慢一些,但能通)direct:P2P 打洞成功(更快、更稳定)
9. 常见疑难杂症(只保留“现象级”问题)
Q1:一直走很远的 DERP(例如 DERP(nue)),不走自己的 REGION_CODE
优先用裁判命令:
1 | tailscale netcheck |
常见原因:
derp.server.hostname/ipv4没配全或配错- 客户端被代理影响(
netcheck会出现tshttpproxy: using proxy ...)
Q2:客户端日志出现 dial tcp4 SERVER_PUBLIC_IP:3477: i/o timeout
典型“反代场景端口混淆”:
- 客户端误以为 DERP 对外端口是 3477
- 但 3477 只在本机回环监听,公网必超时
根治:确保 server_url 是 https://hs.example.com,并在 derp.server 明确:
hostname: hs.example.comipv4: SERVER_PUBLIC_IP
Q3:macOS 节点名变成 invalid-xxxxx
Headscale 对 hostname 限制严格(小写字母/数字/-/.)。macOS 设备名带中文/空格等会被拒绝。
修复:在服务器端用 headscale nodes rename 改“显示名”(需要节点 ID;先用 headscale nodes list 查到 ID 即可)。示例(一步):
1 | sudo /usr/local/bin/headscale nodes rename -i <node-id> mac-home |
Q4:tailscale debug derp REGION_CODE 里 IPv6 报错,但 IPv4 OK
域名只有 A 记录、无 AAAA 且服务器无公网 IPv6 时属于正常探测失败;只要实际 tailscale ping 正常即可忽略。
10. 进阶(可选):MagicDNS / Split DNS / Subnet Router
10.1 MagicDNS:用机器名互访
开启 MagicDNS 时 必须配置 base_domain,并且建议显式设置 nameservers.global:
1 | dns: |
客户端侧需要接收 DNS 下发:
1 | sudo tailscale up --accept-dns=true |
10.2 Split DNS:只分流特定后缀到公司 DNS
模板(示例):
1 | dns: |
关键前提:如果 COMPANY_DNS_IP 在公司内网网段里,你在家里必须先通过 Subnet Router 把这个网段路由进 Tailnet,否则会出现“DNS 服务器不可达”。
10.3 Subnet Router:把公司内网网段“桥接进 Tailnet”
在公司内网找一台长期在线且已入网的机器,执行(示例网段):
关键步骤:开启内核 IP 转发(Subnet Router 必须)
1
2
3 echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf
1 | sudo tailscale up \ |
在 headscale 上批准路由:
1 | sudo /usr/local/bin/headscale routes list |
在家里的机器上接收路由:
1 | sudo tailscale up --accept-routes |