Headscale 部署和 DERP 服务器配置

如何连接公司、家庭服务器、公网服务器是一个经常被讨论的问题。一直通过 WireGuard 组网作为核心方式、ZeroTier 作为备用方式。WireGuard 的配置和使用比较复杂但是网络链路清晰,而 ZeroTier 可以保证 WireGuard 链路异常的时候作为备用方式连接网络,通过自定义 Moon 服务器可以提高速度和稳定性。

基于 WireGuard 的 Tailscale 很好的解决了 WireGuard 的配置问题,但是需要依赖 Tailscale 的控制和中继服务器。Headscale 是开源的 Tailscale 控制服务器,摆脱了 Tailscale 的控制,而且相比 ZeroTier 可以更简单的自定义控制服务器和中继服务器,所以使用 Headscale 配置另一个备用网络。

使用 Docker 部署 Headscale

因为控制节点和中继节点都在国内,不打算备案所以无法使用域名,所以无论控制节点还是中继节点都使用 IP 地址的方式。

参考文档 https://github.com/juanfont/headscale/blob/main/docs/running-headscale-container.md

cd /home/ubuntu
mkdir -p ./headscale/config
cd ./headscale
touch ./config/db.sqlite

wget -O ./config/config.yaml https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml
# vim config/config.yaml 修改里面的 server_url 为服务器的 IP 地址和端口,需要在服务器上开放对应端口
# 另外需要注意 nameservers 默认为 1.1.1.1 在国内并不稳定,可以换成其他服务器,具体可以参考最后的详细配置文件
# 假设配置的 server_url 为 http://11.22.33.44:8030/ ,后面会继续用到

# 启动容器
docker run \
  --name headscale \
  --detach \
  --restart always \
  --volume /home/ubuntu/headscale/config:/etc/headscale/ \
  --publish 0.0.0.0:8030:8030 \
  --publish 127.0.0.1:9090:9090 \
  headscale/headscale:0.21 \
  headscale serve

# 查看统计信息
curl http://127.0.0.1:9090/metrics

# 创建用户
docker exec headscale \
  headscale users create phyng

macOS 配置

Tailscale 的客户端配置比 WireGuard 简单,Headscale 稍微麻烦一点,总之就是找到自定义服务端地址的方法,然后生成 nodekey 到服务端注册即可,或者服务端预先生成 authkey,然后直接使用 authkey 登录。

# 安装
brew install --cask tailscale

# 客户端登录
tailscale up --login-server http://11.22.33.44:8030 --force-reauth

# 前往服务器确认登录
docker exec headscale \
  headscale nodes register --user phyng --key nodekey:******

Linux/Ubuntu 配置

# https://tailscale.com/download/linux
curl -fsSL https://tailscale.com/install.sh | sh

tailscale up --login-server http://11.22.33.44:8030 --force-reauth

# 前往服务器确认登录
docker exec headscale \
  headscale nodes register --user phyng --key nodekey:******

Windows 配置

先去 Tailscale 官网下载 Windows 客户端 安装包,正常安装但是不启动。然后参考文档 https://github.com/juanfont/headscale/blob/main/docs/windows-client.md 修改注册表配置,可以直接用下面的命令修改。

# 以管理员身份打开 PowerShell 执行注册表修改,或者通过注册表编辑器修改
New-ItemProperty -Path 'HKLM:\Software\Tailscale IPN' -Name UnattendedMode -PropertyType String -Value always
New-ItemProperty -Path 'HKLM:\Software\Tailscale IPN' -Name LoginURL -PropertyType String -Value http://11.22.33.44:8030

修改后启动 Tailscale,会显示登录页面,找到 nodekey:xxxxx,然后在服务器上执行

# 前往服务器确认登录
docker exec headscale \
  headscale nodes register --user phyng --key nodekey:******

群晖 Synology 配置

群晖可以通过 authkey 登录,不需要 nodekey

# 群晖可以直接在应用中心搜索安装 Tailscale 或者参考 https://tailscale.com/kb/1131/synology/ 手动在 https://pkgs.tailscale.com/stable/#spks 下载文件安装

# 服务端生成 authkey
docker exec headscale \
  headscale --user phyng preauthkeys create --reusable --expiration 24h

# 进入群晖终端,使用 authkey 登录
sudo tailscale up --reset --login-server http://11.22.33.44:8030 --authkey ****** --force-reauth

iPhone 配置

新版 Tailscale 已经支持自定义服务器地址了,用非国区账号下载 Tailscale,在系统设置找到 Tailscale,输入服务器地址,打开 App 会弹出登录页面显示 nodekey:xxxxx,然后在服务器上执行命令即可。

docker exec headscale \
  headscale nodes register --user phyng --key nodekey:******

Android 配置

首先在 https://f-droid.org/en/packages/com.tailscale.ipn/ 下载 APK 安装。安装之后先切换登录服务器地址再登录,首先点击右上角三个点菜单,然后点击其他区域关闭菜单,重复几次就能看到修改服务器地址的菜单,修改之后保存,再点击使用其他方式登录即可自动打开自定义服务器的登录页面,找到里面的 nodekey:xxxxx 然后在服务器上执行命令即可,参考 iPhone 的命令。

DERP 中继服务器配置

因为备案的问题,这里使用纯 IP 实现,参考文档 https://icloudnative.io/posts/custom-derp-servers/#使用纯-ip

# 默认使用 443 端口,如果需要修改可以参考文档评论区
docker run --restart always --net host --name derper -d ghcr.io/yangchuansheng/ip_derper

创建一个 DERP 配置的 JSON 文件命名为 derp.json ,可以上传到任意可以公开访问的地方,然后在 config.yaml 中填写对应的 URL 即可。中继服务器可以和控制节点在同一台服务器上,也可以是不同的服务器。

{
  "Regions": {
    "901": {
      "RegionID": 901,
      "RegionCode": "sh2",
      "RegionName": "Shanghai-2",
      "Nodes": [
        {
          "Name": "901a",
          "RegionID": 901,
          "DERPPort": 443,
          "HostName": "11.22.33.44",
          "IPv4": "11.22.33.44",
          "InsecureForTests": true
        }
      ]
    }
  }
}

ACL 权限控制配置

创建一个 acls.hujson 文件和 config.yaml 放在同一目录下。acls.hujson 和正常的 JSON 格式类似但是允许使用 // 注释。下面是一个简单配置示例。如果不配置 ACL,默认是允许所有机器间的流量。

下面的配置实现了 100.64.0.1 允许访问全部机器,而 100.64.0.6 可以访问 100.64.0.7 和 100.64.0.8,不能访问其他机器。更多配置参考文档

{
  // groups are collections of users having a common scope. A user can be in multiple groups
  // groups cannot be composed of groups
  "groups": {
    "group:admin": ["phyng"]
  },

  // hosts should be defined using its IP addresses and a subnet mask.
  // to define a single host, use a /32 mask. You cannot use DNS entries here,
  // as they're prone to be hijacked by replacing their IP addresses.
  // see https://github.com/tailscale/tailscale/issues/3800 for more information.
  "hosts": {
    "m1": "100.64.0.1/32",
    "m2": "100.64.0.6/32"
  },

  "acls": [
    // m1 -> *
    { "action": "accept", "src": ["100.64.0.1"], "dst": ["100.64.0.1:*"] },
    { "action": "accept", "src": ["100.64.0.1"], "dst": ["100.64.0.6:*"] },
    { "action": "accept", "src": ["100.64.0.1"], "dst": ["100.64.0.7:*"] },
    { "action": "accept", "src": ["100.64.0.1"], "dst": ["100.64.0.8:*"] },
    // m2 -> 7/8
    { "action": "accept", "src": ["100.64.0.6"], "dst": ["100.64.0.7:*"] },
    { "action": "accept", "src": ["100.64.0.6"], "dst": ["100.64.0.8:*"] }
  ]
}

完整配置示例

下面是一个完整的 config.yaml ,相比默认配置去掉了一些注释,增加了本文相关的注释。注意里面引用了前面提到的 derp.jsonacls.hujson 文件。

---
# 填写自己的服务器公网 IP 地址和端口这里自定义端口为 8030可以换成别的
server_url: http://11.22.33.44:8030
listen_addr: 0.0.0.0:8030
metrics_listen_addr: 0.0.0.0:9090
grpc_listen_addr: 127.0.0.1:50443
grpc_allow_insecure: false
private_key_path: ./private.key
noise:
  private_key_path: ./noise_private.key

# IPV4 在前 tailscale status 默认才能输出 IPV4 地址
ip_prefixes:
  - 100.64.0.0/10
  - fd7a:115c:a1e0::/48

derp:
  server:
    enabled: false
    region_id: 999
    region_code: 'headscale'
    region_name: 'Headscale Embedded DERP'
    stun_listen_addr: '0.0.0.0:3478'
  urls:
    # 这里将 derp.json 上传到了阿里云可以替换成其他任意可用的地址
    - https://my-bucket.aliyuncs.com/public/apps/headscale/derp.json
    - https://controlplane.tailscale.com/derpmap/default
  paths: []
  auto_update_enabled: true
  update_frequency: 24h

disable_check_updates: false
ephemeral_node_inactivity_timeout: 30m
node_update_check_interval: 10s
db_type: sqlite3
db_path: ./db.sqlite
acme_url: https://acme-v02.api.letsencrypt.org/directory
acme_email: ''
tls_letsencrypt_hostname: ''
tls_letsencrypt_cache_dir: ./cache
tls_letsencrypt_challenge_type: HTTP-01
tls_letsencrypt_listen: ':http'
tls_cert_path: ''
tls_key_path: ''

log:
  format: text
  level: info

# 权限控制文件相对路径
acl_policy_path: './acls.hujson'

dns_config:
  override_local_dns: true
  nameservers:
    - 8.8.8.8
    - 114.114.114.114
    - 223.5.5.5
  domains: []
  magic_dns: true
  # 可以自定义为自己的域名
  base_domain: example.com

unix_socket: ./headscale.sock
unix_socket_permission: '0770'
logtail:
  enabled: false
randomize_client_port: false

IP 显示优化

Headscale 默认配置文件 ip_prefixes 默认是 IPV6 在前,这样导致添加机器后运行 tailscale status 默认显示 IPV6 地址,而不是 IPV4 地址,不是很方便。

➜ tailscale status
fd7a:115c:a1e0::1 x***             phyng        macOS   -
fd7a:115c:a1e0::4 xx***            phyng        linux   active; relay "***", tx 97124 rx 79692
100.64.0.5      xxx***               phyng        iOS     offline
100.64.0.2      xxxx***           phyng        linux   idle, tx 195700 rx 419468

这是因为前述 config.yaml 中的配置默认 IPV6 在前:

ip_prefixes:
  - fd7a:115c:a1e0::/48
  - 100.64.0.0/10

如果刚配置还没有添加机器。可以修改这个配置,把 IPV4 放在前面即可。

ip_prefixes:
  - 100.64.0.0/10
  - fd7a:115c:a1e0::/48

但是如果已经添加过机器,修改配置之后只能保证新的机器默认显示 IPV4 地址,已经添加的机器还是会显示 IPV6 地址,这样 tailscale status 输出的结果无法对齐。这种情况可以直接修改 sqlite3 数据库文件。

# Ubuntu/Debian 安装 sqlite3
sudo apt install sqlite3

# 进入 sqlite3 数据库
sqlite3 config/db.sqlite

进入之后通过分析发现机器数据存储在 machines 表里面,ip_addresses 字段存储的是 IP 地址,以逗号分隔,根据分析结果可以直接修改。

-- 设置显示模式
.mode column
.headers on

-- 查看全部机器配置
select * from machines;

-- 查看 machines 表里面的 IP 配置
select id, ip_addresses from machines;

-- 调换 ip_addresses 的顺序让 IPV4 靠前
update machines set ip_addresses = '100.64.0.2,fd7a:115c:a1e0::2' where ip_addresses = 'fd7a:115c:a1e0::2,100.64.0.2';
update machines set ip_addresses = '100.64.0.3,fd7a:115c:a1e0::3' where ip_addresses = 'fd7a:115c:a1e0::3,100.64.0.3';
update machines set ip_addresses = '100.64.0.4,fd7a:115c:a1e0::4' where ip_addresses = 'fd7a:115c:a1e0::4,100.64.0.4';
update machines set ip_addresses = '100.64.0.1,fd7a:115c:a1e0::1' where ip_addresses = 'fd7a:115c:a1e0::1,100.64.0.1';

-- 退出数据库编辑
.quit

实际测试发现,Headscale 应该会从 sqlite3 刷新数据同步到机器,要快速生效的话可以直接重启 headscale 让它从数据库读取最新的数据,这样所有客户端运行 tailscale status 的时候默认看到的就是最新的 IPV4 了。

docker restart headscale

headscale 命令行工具

# 列出全部节点
docker exec headscale headscale node ls

# 删除某个节点
docker exec -ti headscale headscale node delete -i 8

headscale 自定义 DNS 配置

可以自由的配置 DNS,比如添加 A 记录等等。

dns_config:
  extra_records:
    - name: 'www.example.com'
      type: 'A'
      value: '100.64.0.7'
    - name: 'admin.example.com'
      type: 'A'
      value: '100.64.0.7'