前言
2024 年开始在家里自建服务器之后,一直使用的都是 ZeroTier 来实现服务器之间的组网,虽然它的使用体验不错,但毕竟是第三方服务,偶尔会有不稳定的情况发生。
但如果自建的话,又因为国内云服务器的网络带宽一直都很贵,自建的投入产出不成正比,于是就搁置了。
不过 2025 年末腾讯云和阿里云都上了峰值 200Mbps 带宽的服务器,价格也不算太贵了,于是就来尝试一下吧。
方案概述
我采用的方案是:Headscale(服务端,内置 DERP 流量中继服务)+ Tailscale(客户端)。
优点包括:
- 方案成熟,
Headscale是 37.3K GitHub Star 的项目,社区活跃,更新频繁 Tailscale客户端支持广泛,包括 Windows、Mac、Linux、iOS、Android 等多个平台Headscale内置了DERP流量中继服务,在客户端之间无法直接建立连接时,可以通过DERP服务器进行流量中转,保证了连接的稳定性和可靠性
而关于版本,Headscale 是 0.28.0,Tailscale 是 1.90.9,这非常重要,因为在测试过程中发现 Tailscale 的某些版本和 Headscale 之间存在兼容性问题,导致 SSH 功能无法使用,所以一定要注意版本的选择!
回到本次部署,实施的具体步骤为:
- 安装
Headscale服务端并配置内置的DERP服务 - 配置
Caddy反向代理 - 配置
Headscale的用户 - 配置
Tailscale客户端并连接到Headscale服务端- 在有公网 IP 的 Linux 设备上配置
- 在无公网 IP 的 Linux 设备上配置
- 在 iOS 移动设备上配置
- 测试
Tailscale客户端之间的连接- 测试
tailscale ping - 测试
tailscale ssh - 测试
ping - 测试
ssh - 测试 Web 访问
- 测试
- 其他补充
Headscale 注册用的域名为 https://headscale.senjianlu.com,部署在日本东京的服务器上,使用 Caddy 反向代理到 Headscale 服务端的监听地址。
其他的服务器、端口和 IP 地址等信息如下:
| 设备名称 | 公网 IP | 地区 | 私网 IP | 备注 |
|---|---|---|---|---|
| tailscale-server-01 | 44.246.36.9 | 美国 | 10.64.0.1 | 有公网 IP 的服务器 |
| tailscale-server-02 | / | 美国 | 10.64.0.2 | 无公网 IP 的服务器 |
| the-ios-device | / | 中国 | 10.64.0.3 | iOS 移动设备 |
| headscale-server | 18.183.229.79 | 日本 | / | 部署 Headscale 服务端的服务器,内置 DERP 服务 |
-
headscale-server开放的端口:端口 协议 备注 22 TCP SSH 端口,供管理员远程登录使用 80 TCP HTTP,供 Caddy反向代理使用443 TCP HTTPS,供 Caddy反向代理使用3478 UDP DERP服务的 STUN 监听端口,供客户端进行 NAT 穿透使用 -
tailscale-server-01开放的端口:端口 协议 备注 22 TCP SSH 端口,供管理员远程登录使用 41641 TCP/UDP Tailscale客户端的通信端口,供客户端之间建立连接使用 -
the-ios-device与tailscale-server-02没有公网 IP,因此不用特意配置安全组。
操作步骤
一、安装 Headscale 服务端并配置内置的 DERP 服务
考虑到 Headscale 中的 DERP 涉及到网络穿透,部署在 Docker 中可能会有一些麻烦,所以我选择直接在服务器上安装 Headscale 服务端。
首先下载 Headscale 的 Linux AMD64 版本的安装包并安装:
cd /root/
wget https://github.com/juanfont/headscale/releases/download/v0.28.0/headscale_0.28.0_linux_amd64.deb
sudo dpkg -i headscale_0.28.0_linux_amd64.deb
然后确认下默认的配置文件路径和数据目录:
cat /etc/headscale/config.yaml
内容应该是和 GitHub 仓库中最新的文件 config-example.yaml 一致的。
我这里备份并精简下:
# 备份原始配置文件
mv /etc/headscale/config.yaml /etc/headscale/config.yaml.bak
# 创建新的配置文件
nano /etc/headscale/config.yaml
# 需要设置一个唯一的 server_url,客户端会通过这个 URL 来连接到服务端
server_url: https://headscale.senjianlu.com
# 由于后续会使用 Caddy 反向代理,所以这里的 listen_addr 可以设置为本地地址和端口
listen_addr: 0.0.0.0:8080
# 监控和 gRPC 的监听地址设置为本地地址和端口,不要暴露在公网
metrics_listen_addr: 127.0.0.1:9090
grpc_listen_addr: 127.0.0.1:50443
grpc_allow_insecure: false
noise:
private_key_path: /var/lib/headscale/noise_private.key
# 网段配置保持默认
prefixes:
v4: 10.64.0.0/10
v6: fd7a:115c:a1e0::/48
allocation: sequential # 顺序分配 IP 地址
derp:
server:
enabled: true
region_id: 999
region_code: "headscale-jp"
region_name: "Japan"
verify_clients: true # 防白嫖关键
stun_listen_addr: "0.0.0.0:3478"
private_key_path: /var/lib/headscale/derp_server_private.key
automatically_add_embedded_derp_region: true # 自动告诉客户端内置的 DERP 服务器信息
ipv4: 18.183.229.79 # 这里填你的公网 IP
ipv6: 2406:da14:cdc:d900:de8b:893a:a25d:b67c # 这里填你的公网 IPv6 地址,如果没有 IPv6 可以留空
urls: [] # 禁用官方 DERP 列表,实现纯私有化
paths: []
auto_update_enabled: true
update_frequency: 3h
node:
expiry: 0 # 固定节点永不过期
ephemeral:
inactivity_timeout: 30m # Docker 等连接时会带有 --ephemeral 标志,30 分钟不活跃就过期
# 数据库信息使用默认的即可
database:
type: sqlite
debug: false
gorm:
prepare_stmt: true
parameterized_queries: true
skip_err_record_not_found: true
slow_threshold: 1000
sqlite:
path: /var/lib/headscale/db.sqlite
write_ahead_log: true
wal_autocheckpoint: 1000
policy:
mode: file
path: "/etc/headscale/acl.json"
dns:
magic_dns: true
# 之后可以通过 hostname.tailscale.internal 来访问对应的设备
base_domain: tailscale.internal
override_local_dns: true
nameservers:
global:
- 223.5.5.5 # 阿里 DNS
- 119.29.29.29 # 腾讯 DNS
- 1.1.1.1 # Cloudflare DNS
- 2400:3200::1 # 阿里 IPv6 DNS
unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"
保存后,再创建一份上述提到的 /etc/headscale/acl.json 文件,来配置访问控制列表(ACL)。
这里我的配置是允许同一用户下的设备之间可以互相访问,并且允许 SSH 访问;而用户之间的设备则是隔离的:
nano /etc/headscale/acl.json
{
"acls": [
{
"action": "accept",
"src": ["autogroup:member"],
"dst": ["*:*"]
}
],
"ssh": [
{
"action": "accept",
"src": ["autogroup:member"],
"dst": ["autogroup:member"],
"users": ["autogroup:nonroot", "root"]
}
]
}
然后重新启动 Headscale 服务:
sudo systemctl restart headscale
# 检查服务状态,确保没有报错
sudo systemctl status headscale
# 设置开机自启
sudo systemctl enable headscale
# 查看日志
sudo journalctl -u headscale -f
二、配置 Caddy 反向代理
如果你还没有安装 Caddy,可以直接拷贝官网的安装命令:Debian, Ubuntu, Raspbian
修改 Caddy 的配置文件,添加一个反向代理的配置:
sudo nano /etc/caddy/Caddyfile
headscale.senjianlu.com {
# 反向代理到 Headscale 服务端的监听地址
reverse_proxy localhost:8080 {
# 确保支持 WebSocket(DERP 某些模式需要)和长连接
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
# TLS 配置会自动启用,Caddy 会使用 Let's Encrypt 来获取证书
# 日志配置,记录访问日志和错误日志
log {
output file /var/log/caddy/headscale.log
}
}
保存并退出后,重新加载 Caddy 配置:
sudo systemctl reload caddy
此时你就可以通过 https://headscale.senjianlu.com 访问 Headscale 服务端了,如果你用浏览器直接打开的话显示的应该是一个空白的页面。
三、配置 Headscale 的用户
In headscale, a node (also known as machine or device) is typically assigned to a headscale user. Such a headscale user may have many nodes assigned to them and can be managed with the headscale users command. Invoke the built-in help for more information: headscale users —help.
在 Headscale 中,节点(也称为机器或设备)需要被分配给一个 Headscale 用户。
不同用户之间的节点是相互隔离的,默认情况下用户只能看到和管理自己名下的节点,不过你也可以通过 ACL 来实现不同用户之间的访问控制,这个之后会单独出一篇文章来讲。
首先看下现在有没有用户:
headscale users list
正常情况下应该是没有用户的,接下来创建两个用户:
# 个人设备用的用户(实际并未在此次实践中使用)
headscale users create rabbir
# 生产服务器用的用户
headscale users create server
创建好用户之后进入下一步。
四、配置 Tailscale 客户端并连接到 Headscale 服务端
1、在有公网 IP 的 Linux 设备上配置
首先在有公网 IP 的 Linux 设备上安装 Tailscale 客户端
curl -fsSL https://tailscale.com/install.sh | sh
安装完成后,使用以下命令连接到 Headscale 服务端。
注意,这里不要使用 --ssh 参数!否则 Tailscale 将会接管设备间 SSH 流量,这意味着你必须去 Headscale 设置 SSH 相关的 ACL 规则,会增加很多工作量。
sudo tailscale up --login-server https://headscale.senjianlu.com \
--accept-dns=true \
--hostname tailscale-server-01
正常情况下会显示以下内容:
To authenticate, visit:
https://headscale.senjianlu.com/register/mgv6JKmzwJikhm3vw6k3pRD8
回到 Headscale 服务端的控制台,接受这个设备的注册请求并将其分配给 server 用户:
headscale nodes register --user server --key mgv6JKmzwJikhm3vw6k3pRD8
再次回到 Linux 设备上,等待几秒钟后,应该就能看到打出 Success. 的提示了。
这时我们可以通过 tailscale status 命令来查看当前设备的状态,或者在 Headscale 服务端的控制台上查看当前用户下的节点列表:
headscale nodes list --user server
如果你和我的私网网段配置一样的话,由于是第一台设备,因此这台有公网 IP 的服务器其私网 IP 为 10.64.0.1。
如果需要的话,你可以通过下面的命令修改这个设备的名字:
headscale nodes rename -i <ID> <新名字>
2、在无公网 IP 的 Linux 设备上配置
实际操作和上面有公网 IP 的 Linux 设备上配置是一样的,同样注册给 server 用户即可。
最终私网 IP 被分配为 10.64.0.2。
3、在 iOS 移动设备上配置
首先需要在 App Store 使用非中国区的 Apple ID 下载并安装 Tailscale 客户端,之后照着下面图中的操作配置自定义 Headscale 域名,获取 Key 后像上面的 Liunx 设备一样回到 Headscale 所在服务器进行注册即可:

headscale nodes register --user rabbir --key YwToxy68Lvy6e2J1--XXxXxx
无视图中的 IP,这里最终 iOS 设备的私网 IP 为 10.64.0.3。
五、测试 Tailscale 客户端之间的连接
1、测试 tailscale ping
我这里在 tailscale-server-01 的服务器上进行测试。
首先各节点状态:
tailscale status
10.64.0.1 tailscale-server-01 server linux -
fd7a:115c:a1e0::3 tailscale-server-02 server linux active; direct 172.26.13.40:41641
看到 10.64.0.1 服务器在线,tailscale ping 一下:
tailscale ping 10.64.0.2
pong from tailscale-server-02 (10.64.0.2) via 172.26.13.40:41641 in 1ms
没有走中继,而是自动切换到(内网)直连模式了,完美!
2、测试 tailscale ssh
依然在 tailscale-server-01 的服务器上进行测试,尝试通过 Tailscale 的 SSH 功能连接到 tailscale-server-02:
tailscale ssh root@tailscale-server-02
Dial(“10.64.0.2”, 22): unexpected HTTP response: 502 Bad Gateway, dial failure: dial tcp 10.64.0.2:22: connect: connection timed out
kex_exchange_identification: Connection closed by remote host
Connection closed by UNKNOWN port 65535
在等了很久、像是卡住了之后,报了上面这条信息,很明显是失败了。
百思不得其解,在确定了以下内容都没有问题之后:
tailscale-server-01和tailscale-server-02服务器的安全组都开放了22与41641端口tailscale-server-01和tailscale-server-02的 ufw 是关闭的,并且 iptables 规则也没有问题tailscale-server-01和tailscale-server-02的 sshd 服务都是正常运行的Tailscale客户端注册的时候没有携带--ssh参数如果携带了的话,用下面的命令进行强制重新注册:
sudo tailscale up --login-server https://headscale.senjianlu.com \ --accept-dns=true \ --hostname tailscale-server-01 --force-reauth \ --resetHeadscale服务端的 ACL 配置允许同一用户下的设备之间可以互相访问,并且允许 SSH 访问你可以在
Tailscale客户端上执行下面的命令:tailscale status只要底下不存在这条信息,就说明 ACL 配置是没问题的:
Tailscale SSH enabled, but access controls don’t allow anyone to access this device. Update your tailnet’s ACLs to allow access.
最终,我只能怀疑是 Tailscale 和 Headscale 之间的某些兼容性问题导致的了。
在这个时间点,Headscale 的版本是 0.28.0,而 Tailscale 的版本是 1.96.4,这与 Headscale 官方 Release Notes 中提到最小支持版本 v1.74.0 相差确实有点大。
于是尝试降级 Tailscale 客户端到 1.90.x:
# 查看可用版本
apt-cache madison tailscale
# 退回到 1.90.9 版本
sudo apt install tailscale=1.90.9
# 完成后确认版本
tailscale version
# 锁定版本,防止自动升级
sudo apt-mark hold tailscale
1.90.9
tailscale commit: 6e8a4f2de795ae6801f4b599e3f64ca6b5465c01
long version: 1.90.9-t6e8a4f2de-g19196f361
other commit: 19196f3617f75c3898c4fdfd4e82dcf9d258a04b
go version: go1.25.3
再次尝试 tailscale ssh:
tailscale ssh root@tailscale-server-02
No ED25519 host key is known for tailscale-server-02 and you have requested strict checking.
Host key verification failed.
虽然报了条错误信息,但是这证明连接已经成功了,只是经由 Tailscale 的 SSH 认证还存在问题,考虑到正常情况下不会使用 tailscale ssh 来连接服务器,而是直接使用 ssh 命令来连接,所以我就不纠结这个问题了。
我也真服了,各种排查花了近 8 个小时…
3、测试 ping
在 tailscale-server-01 上 ping tailscale-server-02 的私网 IP:
ping 10.64.0.2
ping tailscale-server-02
ping tailscale-server-02.tailscale.internal
都没有问题,说明 Tailscale 的内置 DNS 解析也正常工作了。
4、测试 ssh
在 tailscale-server-01 上通过普通的 ssh 命令连接到 tailscale-server-02:
ssh ubuntu@10.64.0.2
ssh ubuntu@tailscale-server-02
ssh ubuntu@tailscale-server-02.tailscale.internal
同样能够成功连接,说明 Tailscale 的 SSH 功能和内置 DNS 解析都正常工作了。
5、测试 Web 访问
在 tailscale-server-02 上部署一个简单的 Web 服务,监听在 80 端口:
sudo apt install nginx
sudo systemctl start nginx
在 tailscale-server-01 上通过 curl 命令访问 tailscale-server-02 的私网 IP:
curl http://10.64.0.2:80
curl http://tailscale-server-02:80
curl http://tailscale-server-02.tailscale.internal:80
如果能够正常返回 nginx 的默认欢迎页面的 HTML 内容,就说明 Web 访问也成功了。
至此,Headscale 和 DERP 的部署以及 Tailscale 客户端的配置和连接测试就全部完成了!
六、其他补充
我在通过下面的命令查看 Headscale 日志的时候:
journalctl -u headscale -f
还看到过不少类似这种报错信息:
ERR http internal server error error=“noise upgrade failed: noise handshake failed: decrypting machine key: chacha20poly1305: message authentication failed” code=500
如果你确保自己所有的 Tailscale 都显示在了节点列表中且是在线状态的话,那就完全不用上面认证失败的错误,这大概率是有其他人在批量扫描无需认证的 DERP 节点。
结束。