Headscale自托管Tailscale:安装与首个节点接入
在Ubuntu 24.04 VPS上自托管Tailscale控制服务器。本文使用官方headscale 0.29.3 .deb,说明先设置server_url再启动服务,并接入首个节点。
headscale 是什么
Headscale 是 Tailscale 控制服务器的自托管实现。因此,协调您的私有网络的计算机是您拥有的 VPS。它是一个社区项目,不由 Tailscale Inc. 运营。每台计算机仍运行官方 tailscale 客户端,只需通过一个标志 --login-server 将其指向您的服务器。
控制服务器负责记录哪些设备属于该网络。它从 100.64.0.0/10 中为每个节点分配地址,分发公钥,并告知节点彼此的位置。隧道仍使用 WireGuard,在节点之间直接建立。您的两台计算机之间的流量不会经过 headscale 服务器,除非无法建立直接路径,节点转而使用中继。
每个 headscale 实例提供一个 tailnet(一个 Tailscale 网络)。项目将其描述为适合个人使用或小型组织。只有三四台计算机时,在您拥有的 VPS 上运行普通的 WireGuard VPN需要维护的软件更少,也更不容易出问题。每当您不想为每台新笔记本电脑手动编写一个 [Peer] 配置块时,headscale 就更有价值。有关这两种模式的详细比较,请参阅WireGuard 与 Tailscale 的区别。
安装前的准备条件
- 一台运行 Ubuntu 24.04 的 VPS,具有公共 IPv4 地址和 sudo 访问权限。如果服务器是新建的,请先完成新 VPS 上的前 10 分钟。
- 一个指向该地址的 DNS A 记录。本指南使用
headscale.example.com。 - 用于 MagicDNS 的另一个域名或子域名。本指南使用
tailnet.example.net。它不能与server_url中的域名相同。 - 一台用于加入网络的客户端计算机,运行 Linux、macOS、Windows、Android 或 iOS。
从官方 .deb 安装 headscale
该项目会在 GitHub releases 页面发布 .deb 软件包。截至 2026 年 7 月,当前版本为 0.29.3。请先检查架构,因为文件名包含架构信息。
sudo apt update
sudo apt install -y wget
dpkg --print-architecture在普通的 x86 VPS 上,该命令会输出 amd64;在 Ampere 或 Graviton 类套餐上,会输出 arm64。将结果放入下面的变量中。
HEADSCALE_VERSION="0.29.3"
HEADSCALE_ARCH="amd64"
wget --output-document=headscale.deb \\
"https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_${HEADSCALE_ARCH}.deb"
sudo apt install -y ./headscale.deb
headscale version文件名前的 ./ 是必需的。没有它,apt 会在软件源中查找名为 headscale.deb 的软件包,并执行失败。
该软件包会创建 headscale 系统用户,写入默认的 /etc/headscale/config.yaml,并安装 systemd 单元。它不会启动服务,这样的顺序是正确的。随软件包提供的配置将 server_url 指向 http://127.0.0.1:8080。你的任何客户端都无法访问该地址,因此此时启动服务会导致配置错误,即使服务能够启动也是如此。此时运行 sudo systemctl is-active headscale 会输出 inactive。这是预期行为,不是故障。
启动服务前配置 server_url
使用 sudo nano /etc/headscale/config.yaml 编辑 /etc/headscale/config.yaml,或使用 sed 应用相同的三项更改。保留原始文件副本,因为该文件较长且包含大量注释,是其余设置的最佳参考。
sudo cp /etc/headscale/config.yaml /etc/headscale/config.yaml.orig
sudo sed -i 's|^server_url:.*|server_url: https://headscale.example.com|' /etc/headscale/config.yaml
sudo sed -i 's|^listen_addr:.*|listen_addr: 127.0.0.1:8080|' /etc/headscale/config.yaml
sudo sed -i 's|^ base_domain:.*| base_domain: tailnet.example.net|' /etc/headscale/config.yaml
sudo grep -E '^(server_url|listen_addr):|^ base_domain:' /etc/headscale/config.yamlserver_url 是 headscale 写入每个客户端注册信息的地址。客户端此后会一直拨号连接到这个确切字符串,因此这里必须填写带有 https:// 的公网名称,不能填写 127.0.0.1。
listen_addr 是进程绑定的地址。将其保留为回环地址。同一服务器上的反向代理负责终止 TLS(传输层安全)并将请求转发到该地址,因此服务器外部无需访问端口 8080。
base_domain 是 MagicDNS 后缀,即节点使用的域名。它必须是没有末尾句点的完全限定域名,并且必须与 server_url 中的域名不同,否则两个名称空间会发生冲突。
不要修改数据库部分。默认数据库是位于 /var/lib/headscale/db.sqlite 的 SQLite,所在目录由软件包创建并拥有。对于这种规模的 tailnet,SQLite 已经足够。
启动 headscale 并确认其正在运行
sudo systemctl enable --now headscale
sudo systemctl is-active headscale
curl -sS -o /dev/null -w '%{http_code}\\n' http://127.0.0.1:8080/healthis-active 输出 active,curl 输出 200。enable --now 同时完成两项工作:启动服务,并将其设置为重启后自动启动。
如果 is-active 输出 failed,请使用 sudo journalctl -u headscale -n 50 --no-pager 读取日志。此阶段的故障几乎总是由配置文件导致,因为 headscale 会在打开套接字前解析整个文件。因此,缩进错误或未知键会在进程开始监听前阻止其启动。修复文件后执行 sudo systemctl restart headscale。之后每次修改配置都需要执行相同的重启操作。客户端随后会自动重新连接。如果您不熟悉 systemd 单元,使用 systemd 运行您自己的服务和计时器介绍了这里使用的命令。
在 shell 中检查状态文件:
stat -c '%U %n' /var/lib/headscale/db.sqlite /var/lib/headscale/noise_private.key两行都以 headscale 开头,这是软件包创建的非特权用户。noise_private.key 是服务器向客户端标识自身的身份文件。请保留它。如果删除该文件,headscale 会生成新的身份,所有节点都必须重新注册。
在 headscale 前置 TLS
客户端必须通过 HTTPS 访问 server_url。Caddy 是最简便的方案,因为它会自动申请和续订证书。
sudo apt install -y caddy将 /etc/caddy/Caddyfile 替换为 headscale 文档中的配置块:
headscale.example.com {
reverse_proxy 127.0.0.1:8080 {
header_up True-Client-IP {remote_host}
header_up X-Real-IP {remote_host}
}
}sudo caddy validate --adapter caddyfile --config /etc/caddy/Caddyfile
sudo systemctl restart caddy
sudo systemctl is-active caddy如果文件解析成功,validate 会输出 adapted config to JSON。提示文件未格式化只是外观问题。在您的笔记本电脑上,curl -sS -o /dev/null -w '%{http_code}\\n' https://headscale.example.com/health 也应输出 200。这项检查即可证明 DNS、防火墙、证书和代理已协同工作。
下面是容易让人排查一晚的代理细节。Tailscale 控制连接使用 HTTP upgrade,通过 POST 而不是 GET 发起,Upgrade 标头的值为 tailscale-control-protocol。Caddy 无需额外配置即可透传这些内容。nginx 则不会,因此 nginx 前端需要以下 upgrade 映射:
map $http_upgrade $connection_upgrade {
default keep-alive;
'' close;
}
server {
listen 443 ssl;
server_name headscale.example.com;
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
proxy_pass http://127.0.0.1:8080;
}
}如果省略这些行,普通请求仍会成功。因此 /health 会返回 200,看起来一切正常,但长连接控制连接始终无法建立,您的节点会注册成功,然后处于离线状态。如果选择 nginx 方案,请参阅 在 Ubuntu 24.04 中使用 nginx 配置 Certbot,了解证书配置部分。
在 UFW 中应开放哪些端口
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose端口 443 承载所有客户端通信。端口 80 仅用于 ACME(自动证书管理环境)HTTP challenge 和重定向到 HTTPS;Caddy 必须使用它才能获取证书。
端口 8080 保持关闭。listen_addr 是 127.0.0.1:8080,因此代理通过 loopback 接口访问 headscale,不涉及任何防火墙规则。向互联网开放 8080 会为客户端提供明文控制通道,但没有任何好处。请注意,大多数提供商会在控制面板中运行第二层防火墙,该防火墙独立于 UFW。因此,端口可能在服务器上已开放,但在网络边界处仍处于关闭状态。VPS 上的 UFW 防火墙基础会更详细地介绍规则语法。
创建用户和预认证密钥
sudo headscale users create alice
sudo headscale users listheadscale 命令是一个客户端。它通过位于 /var/run/headscale/headscale.sock 的 Unix 套接字与正在运行的守护进程通信。该套接字的模式为 0770,所属组为 headscale。这会导致两点。服务停止时,该命令会失败;这也是本指南必须按此顺序操作的另一个原因。此外,您需要使用 sudo,除非将自己的账户添加到 headscale 组。
users list 会在每个名称旁打印一个 ID。您需要这个数字,因为密钥命令接受数字用户 ID,而不是名称。
sudo headscale preauthkeys create --user 1 --expiration 24h密钥只会打印一次。请立即复制它。预认证密钥只能使用一次,除非另行指定,否则有效期为 1 小时。因此,在仍处于测试阶段时,值得设置 --expiration 24h。添加 --reusable 可生成一个供多台机器注册的密钥。请像保护密码一样保护该密钥,因为持有它的任何人都可以加入您的网络。
使用 --login-server 连接第一个客户端
在要加入的计算机上:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --login-server https://headscale.example.com --auth-key 'hskey-auth-PASTE-YOUR-KEY-HERE'
tailscale status
tailscale ip -4tailscale ip -4 会输出 headscale 分配的地址,例如 100.64.0.1。返回服务器后,sudo headscale nodes list 会显示该节点的 ID、用户和在线状态。
--login-server 的值必须与 server_url 完全一致,包括 scheme,且末尾不能有斜杠。系统会将它们作为字符串进行比较。如果不匹配,客户端会向一个地址注册,然后被告知连接另一个地址。
之前登录过 Tailscale 托管服务的计算机会保留该登录状态。先在该计算机上运行 sudo tailscale logout,然后使用 --login-server 运行 tailscale up。
如果省略 --auth-key,客户端会改为输出一个 URL。打开该 URL,页面会显示此次注册尝试的标识符。然后在服务器上批准该注册:
sudo headscale auth register --user alice --auth-id PASTE-THE-ID-FROM-THE-PAGE这种方式更适合您自己的笔记本电脑。对于脚本化操作,预授权密钥更合适,因为不需要人工监控。
DERP,以及直连失败时中继流量的组件
DERP(指定加密数据包中继)是备用路径。当两个节点无法建立直接的 WireGuard 连接时,通常是因为它们都位于严格的 NAT(网络地址转换)之后,此时会改为通过中继发送数据包。中继不会持有任何密钥,因此无法读取您的流量。但它可以看到哪些节点正在通信,以及传输了多少数据。
请明确默认配置的行为。Headscale 默认指向 https://controlplane.tailscale.com/derpmap/default,并使用 auto_update_enabled: true 和 update_frequency: 3h,因此控制平面由您管理,而中继由 Tailscale 提供。对大多数用户来说,这是合理的权衡。如果您不能接受,请自行运行中继。
要运行自己的中继,请在 config.yaml 的 derp.server 下设置 enabled: true,重启 headscale,并使用 sudo ufw allow 3478/udp 开放 STUN(NAT 会话穿越工具)端口。配置文件明确说明了这一要求:server_url 必须使用 https,因为 DERP 需要 TLS。清空 derp.urls 列表会从映射中移除 Tailscale 的中继。如果此时没有可用的内置中继,任何无法直接连接的节点对都将完全无法连接。
在客户端上,tailscale netcheck 会显示其已知的每个中继区域的延迟;tailscale status 会将每个对等节点标记为带有地址的 direct,或带有区域代码的 relay。如果对等节点一直处于 relay,这是 NAT 问题,而不是 headscale 问题。
为什么节点显示为离线?
代理丢弃了升级请求。 这是最常见的原因。其特征是其他部分都正常:/health 返回 200,headscale nodes list 显示该节点,但节点始终无法上线。控制连接是一个携带 Upgrade: tailscale-control-protocol 的 POST 请求。不转发该请求的代理会中断唯一用于报告节点状态的通道。将 nginx 配置与上面的 map 块进行对比,或切换到 Caddy,以排除代理问题。
节点注册后,server_url 发生了变化。 节点会持续连接注册时获取的值。如果您修改了该值,请在每个节点上运行 sudo tailscale up --login-server https://headscale.example.com --force-reauth。
客户端未运行。 在节点上运行 sudo systemctl is-active tailscaled 和 sudo journalctl -u tailscaled -n 50 --no-pager。无法解析或访问您的域名的客户端会在此处记录重试信息。
密钥已过期。 下一节将介绍此问题。
测试时,在 VPS 上运行 sudo journalctl -u headscale -f 以监控服务器端,并在客户端上重启 tailscaled。节点连接到 headscale 后,会立即生成日志行。如果没有任何输出,说明请求尚未到达。因此,请先检查 DNS、防火墙和代理,再检查 headscale。
密钥过期,以及数周后停止工作的节点
存在两种不同的过期机制。混淆它们会浪费时间。
Preauth 密钥会按设计快速过期。默认有效期为 one hour,且只能使用一次。如果 tailscale up 拒绝该密钥,请在服务器上生成新的密钥,不要在客户端编辑任何内容。
节点密钥的有效期较长。config.yaml 的 node 部分设置 expiry: 0,而 0 表示没有默认过期时间:已注册节点会一直有效,直到您使其过期。带标签的节点永不过期。若希望注册信息在一段时间后失效,请设置 expiry: 180d,并明确了解其含义:之后每个未加标签的节点都需要按该周期执行 sudo tailscale up --login-server https://headscale.example.com --force-reauth,而无人重新进行身份验证的无头服务器会自行从网络中断开。
当有人遗失笔记本电脑时,请手动执行此操作。sudo headscale nodes list 会提供节点 ID,随后 sudo headscale nodes expire -i 3 会将该节点注销,sudo headscale nodes delete -i 3 则会将其从网络中完全移除。
备份和升级
/var/lib/headscale 和 /etc/headscale 共同构成整个服务器。复制它们之前先停止服务,因为 SQLite 可能有正在执行的写入操作,在负载下复制数据库可能导致数据不一致。
sudo systemctl stop headscale
sudo tar czf /root/headscale-state.tgz -C /var/lib headscale
sudo tar czf /root/headscale-config.tgz -C /etc headscale
sudo systemctl start headscale
sudo chmod 600 /root/headscale-*.tgz将这两个文件复制到服务器之外。它们包含私钥和所有注册信息,因此需要像保护服务器本身一样妥善保护。从 VPS 执行 restic 备份介绍了如何按计划执行加密备份。
升级流程与安装相同:下载新版 .deb 和 sudo apt install ./headscale.deb,然后重启,并重新运行 is-active 和 /health 检查。从 0.29 开始,升级路径受到严格限制。不能跳过次要版本,也不能降级到更早的次要版本。每次只升级一个次要版本,并在每一步之前进行备份。还要先阅读该版本的发行说明,因为相关版本更改了 ACL 策略行为,并移动了多个配置键。
FAQ
为什么安装 .deb 后 headscale 立即启动失败?
软件包会安装该单元,但不会启动服务,而且默认的 /etc/headscale/config.yaml 只是模板,不是可用的配置。先编辑 server_url、listen_addr 和 base_domain,然后运行 sudo systemctl enable --now headscale,并使用 sudo systemctl is-active headscale 确认状态。如果仍然失败,sudo journalctl -u headscale -n 50 --no-pager 会指出问题所在。在此阶段,问题几乎总是 YAML 错误,因为 headscale 会在绑定端口前解析整个文件。
我还需要在计算机上安装标准 Tailscale 客户端吗?
需要。Headscale 只替代控制服务器。每个节点都运行 Tailscale 提供的官方客户端,并使用 sudo tailscale up --login-server https://headscale.example.com 将其指向您的服务器。标准客户端支持该选项,因此无需修改或重新构建客户端。
我的网络流量会经过 headscale 服务器吗?
通常不会。Headscale 负责协调网络并分发密钥和地址,而数据路径由节点之间的 WireGuard 直接建立。只有当两个节点无法直接互相访问并回退到 DERP 中继时,流量才会绕行。使用随附配置时,这些中继是 Tailscale 的公共中继。在节点上运行 tailscale status,即可查看指定对等节点是 direct 还是通过 relay 连接。
为什么我的节点注册后仍处于离线状态?
如果节点出现在 headscale nodes list 中,却始终不上线,通常是它在反向代理处丢失了控制连接。该连接是一个带有 Upgrade: tailscale-control-protocol 标头、通过 POST 发送的 HTTP 升级请求。除非添加 map $http_upgrade $connection_upgrade 块和匹配的 proxy_set_header 行,否则 nginx 会丢弃该请求。Caddy 无需额外配置即可转发此请求,因此可用于快速判断问题是否出在代理上。
headscale 是否需要域名和 TLS?
实际上需要。客户端会连接到您在 server_url 中填写的字符串。证书是为域名签发的,不能为裸 IP 地址签发;配置文件也规定 DERP 需要 TLS。使用域名和 Caddy 大约需要五分钟,即可获得会自动续期的 HTTPS 端点。通过普通 HTTP 运行控制服务器,意味着每个客户端与服务器之间的通信都会以明文经过互联网。