Skip to content
Go back

部署 Headscale 和 DERP 以通过 Tailscale 实现无公网 IP 服务器之间的组网

| 0 Views Edit page

前言

2024 年开始在家里自建服务器之后,一直使用的都是 ZeroTier 来实现服务器之间的组网,虽然它的使用体验不错,但毕竟是第三方服务,偶尔会有不稳定的情况发生。
但如果自建的话,又因为国内云服务器的网络带宽一直都很贵,自建的投入产出不成正比,于是就搁置了。
不过 2025 年末腾讯云和阿里云都上了峰值 200Mbps 带宽的服务器,价格也不算太贵了,于是就来尝试一下吧。


方案概述

我采用的方案是:Headscale(服务端,内置 DERP 流量中继服务)+ Tailscale(客户端)。 优点包括:

  • 方案成熟,Headscale 是 37.3K GitHub Star 的项目,社区活跃,更新频繁
  • Tailscale 客户端支持广泛,包括 Windows、Mac、Linux、iOS、Android 等多个平台
  • Headscale 内置了 DERP 流量中继服务,在客户端之间无法直接建立连接时,可以通过 DERP 服务器进行流量中转,保证了连接的稳定性和可靠性

而关于版本,Headscale0.28.0Tailscale1.90.9,这非常重要,因为在测试过程中发现 Tailscale 的某些版本和 Headscale 之间存在兼容性问题,导致 SSH 功能无法使用,所以一定要注意版本的选择!

回到本次部署,实施的具体步骤为:

  1. 安装 Headscale 服务端并配置内置的 DERP 服务
  2. 配置 Caddy 反向代理
  3. 配置 Headscale 的用户
  4. 配置 Tailscale 客户端并连接到 Headscale 服务端
    1. 在有公网 IP 的 Linux 设备上配置
    2. 在无公网 IP 的 Linux 设备上配置
    3. 在 iOS 移动设备上配置
  5. 测试 Tailscale 客户端之间的连接
    1. 测试 tailscale ping
    2. 测试 tailscale ssh
    3. 测试 ping
    4. 测试 ssh
    5. 测试 Web 访问
  6. 其他补充

Headscale 注册用的域名为 https://headscale.senjianlu.com,部署在日本东京的服务器上,使用 Caddy 反向代理到 Headscale 服务端的监听地址。
其他的服务器、端口和 IP 地址等信息如下:

设备名称公网 IP地区私网 IP备注
tailscale-server-0144.246.36.9美国10.64.0.1有公网 IP 的服务器
tailscale-server-02/美国10.64.0.2无公网 IP 的服务器
the-ios-device/中国10.64.0.3iOS 移动设备
headscale-server18.183.229.79日本/部署 Headscale 服务端的服务器,内置 DERP 服务
  • headscale-server 开放的端口:

    端口协议备注
    22TCPSSH 端口,供管理员远程登录使用
    80TCPHTTP,供 Caddy 反向代理使用
    443TCPHTTPS,供 Caddy 反向代理使用
    3478UDPDERP 服务的 STUN 监听端口,供客户端进行 NAT 穿透使用
  • tailscale-server-01 开放的端口:

    端口协议备注
    22TCPSSH 端口,供管理员远程登录使用
    41641TCP/UDPTailscale 客户端的通信端口,供客户端之间建立连接使用
  • the-ios-devicetailscale-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 所在服务器进行注册即可:
202603

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-01tailscale-server-02 服务器的安全组都开放了 2241641 端口
  • tailscale-server-01tailscale-server-02 的 ufw 是关闭的,并且 iptables 规则也没有问题
  • tailscale-server-01tailscale-server-02 的 sshd 服务都是正常运行的
  • Tailscale 客户端注册的时候没有携带 --ssh 参数

    如果携带了的话,用下面的命令进行强制重新注册:

    sudo tailscale up --login-server https://headscale.senjianlu.com \
        --accept-dns=true \
        --hostname tailscale-server-01
        --force-reauth \
        --reset
  • Headscale 服务端的 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.

最终,我只能怀疑是 TailscaleHeadscale 之间的某些兼容性问题导致的了。
在这个时间点,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 访问也成功了。

至此,HeadscaleDERP 的部署以及 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 节点。

结束。


Edit page