zhangguanzhang's Blog

[持续更新] - headscale 搭建和应用场景

字数统计: 4k阅读时长: 18 min
2024/07/25

长期记录和更新关于 headscale 部署和应用场景

由来

这几天折腾了下 headscale 部署,发现网上很多部署文章都互相抄袭没有自己的折腾理解,所以写下部署以及讲解一些关键地方,本文章适合有 Linux 基础和部署过 headscale 或者想部署但是感觉市面上文章写得没有适合命令行选手的。

为什么我需要 headscale

之前一直使用 wireguard 组网,各地互访是没问题。

wireguard-simple

但是最近远程 scrcpy 控制家里的手机发现很慢受不了,也就是因为 wireguard 所有端,除了 ecs 以外都是没有公网 IP,都是 ecs 转发而存在木桶效应,即使每个 wireguard 节点有公网 IP,随着端越来越多配置起来也非常繁琐的。

如果只需要一个 ecs 公网 IP,然后各个端能 NAT 打洞直连(例如上面的左边手机和左下角电脑能打洞成功直连,不走 ecs 中继),速度也会不错呢。

nat-th

没错,tailscale 就是也是使用 wireguard 协议(没有内核模块就使用用户态),然后控制平面很强(可以给各个端推送和更新配置,以及还支持 ACL),而 headscale 就是开源的 tailscale 的控制端。这里不讲解接入 tailscale 官方,而是自建 headscale 。

可能有其他人问为啥不用 zerotier 或者其他啥的,那是因为:

  • 首先 wireguard 是 Linux 内核模块,即使不支持的也有用户态 wireguard 的很多成熟轮子
  • tailscale 的多端支持很好,iOS、MacOS 、安卓、win、Linux
  • 并且 tailscale 支持 IPv6,在公司内部已经使用搭建 IPv6 测试环境经过多轮验证了
  • 控制中心即使不用 tailscale 的也可以选择用 headscale
  • tailscale 外国用户群体大,而且 tailscale 有问题基本上 tailscale 开发者会修复

部署

本教程都是 ecs 公网 IP 形式部署,自己有 https 绿锁证书的,相关配置自己研究,文章涉及到的端口需要在 ecs 安全组以及防火墙相关端口自行放行:

协议 端口
tcp headscale web server 端口
tcp headscale 内置 derp 的 https 端口
udp derp 的 stun 端口

机器基础参数自行设置好,例如转发和文件打开数。

历史版本

归档在我 gist 上 headscale

headscale 部署和设置

参考官方文档 running-headscale-linux-manual ,因为 headscale 不依赖 CGO,所以我使用二进制部署,官方这个文档也是隐藏起来的,怎么部署都可以,不一定要和我一样。

截至 2025/01/25 ,从官方仓库看,正式的 release 版本是 v0.24.1,很多文章都是从 main 下载 config-example.yaml 文件后按照他们文章修改字段对应不上。

1
2
3
4
5
6
7
8
9
10

mkdir /etc/headscale/

VER=0.24.1
# 自行解决下载问题 # 有些老系统的 PATH 里没 /usr/local/bin/ ,可以放其他路径里
wget https://github.com/juanfont/headscale/releases/download/v${VER}/headscale_${VER}_linux_amd64
# 下载同版本的示例配置文件
wget -O /etc/headscale/config.yaml https://raw.githubusercontent.com/juanfont/headscale/v${VER}/config-example.yaml

chmod a+x /usr/local/bin/headscale

创建 headscale daemon systemd 后台文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
cat > /etc/systemd/system/headscale.service << EOF
[Unit]
Description=headscale controller
After=syslog.target
After=network.target

[Service]
Type=simple
User=headscale
Group=headscale
ExecStart=/usr/local/bin/headscale serve
Restart=always
RestartSec=5

# Optional security enhancements
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
WorkingDirectory=/var/lib/headscale
ReadWritePaths=/var/lib/headscale /var/run/headscale
AmbientCapabilities=CAP_NET_BIND_SERVICE
RuntimeDirectory=headscale

[Install]
WantedBy=multi-user.target
EOF

因为 headscale 是一个控制中心,不需要特权,我们运行在非 root 用户下,添加用户:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
useradd \
--create-home \
--home-dir /var/lib/headscale/ \
--system \
--user-group \
--shell /usr/sbin/nologin \
headscale

mkdir -p /var/run/headscale/

#创建空的 SQLite 数据库文件和 derp 文件:
touch /var/lib/headscale/db.sqlite /etc/headscale/derp.yaml
chown -R headscale:headscale /var/run/headscale/ /var/lib/headscale
chmod a+r /etc/headscale/config.yaml /etc/headscale/derp.yaml

接下来 vi /etc/headscale/config.yaml 对照下面注释说明, 修改配置文件一些内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# server_url 写外网访问的 ip+端口
# 由于是ecs,所以写公网 IP,以及后面的端口要和 listen_addr 的一致
# 80 443 8080 需要备案
server_url: http://<ecs_public_ip>:8081
# 改为四个0,或者对应公网 IP 的内网网卡 IP 都行
listen_addr: 0.0.0.0:8081

prefixes:
# 因为我机器在阿里云上修改为下面网段,阿里云的 100.63.0.0/10 有用
v4: 100.63.0.0/16
# 如果不希望给设备下发 ipv6 可以注释了
v6: fd7a:115c:a1e0::/48

derp:
server:
# 使用用 headscale 内嵌的 derper 服务器
enabled: false
# 注释掉不使用官方的 derp 服务
#urls: []
# - https://controlplane.tailscale.com/derpmap/default
# 下面写 ecs 公网 IP
ipv4: x.x.x.x

# 关闭 headscale 自动更新,避免 break change 无法启动
disable_check_updates: true

dns:
# 关闭 magic_dns
magic_dns: false
# 设置为你自己的标识,否则后续 tailscale 端连接上显示是 user@example.com
base_domain: xxx

# 下面前俩修改为国内的 dns
nameservers:
global:
- 223.5.5.5
- 223.6.6.6

# 随机端口要打开, tailscale 客户端会使用41641 端口建立 wireguard 链接,这个端口会被中间网络设备阻止
randomize_client_port: true

安装和设置 headscale 补全,因为 headscale 是 daemon 和 cli 两部分,daemon 起来后 cli 很多命令可以操作:

1
2
3
4
apt update
apt install -y bash-completion
headscale completion bash > /etc/bash_completion.d/headscale
. /etc/bash_completion.d/headscale

启动 headscale daemon 进程:

1
2
3
4
5
6
7
8
# 测试文件
headscale configtest

headscale serve
# 配置文件没问题就 ctrl +c 取消掉使用 systemd 启动
chown -R headscale:headscale /var/lib/headscale
systemctl daemon-reload
systemctl enable --now headscale

derp 部署

先ecs 上也部署一个,先查看机器上的 iptables 模式:

1
2
3
4
5
$ iptables -w -V
iptables v1.4.21
iptables v1.8.4 (legacy)
#----
iptables v1.8.9 (nf_tables)

前者都是 legacy 模式,后者是 nf_tables,下面的 tailscale 需要设置 firewall-modeiptables 或者 nftables

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
cat > docker-compose.yml << EOF
services:

# https://tailscale.com/kb/1282/docker
tailscale:
hostname: tailscale
container_name: tailscale
restart: unless-stopped
network_mode: host
image: docker.m.daocloud.io/tailscale/tailscale:v1.74.1
#image: tailscale/tailscale:unstable
cap_add:
- NET_ADMIN
- NET_RAW
- sys_module
volumes:
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
- /lib/modules:/lib/modules
- /run/xtables.lock:/run/xtables.lock:rw
- /var/run/tailscale/:/var/run/tailscale/
- ./tailscale_data:/var/lib/tailscale
devices:
- /dev/net/tun:/dev/net/tun
environment:
TS_EXTRA_ARGS: --advertise-tags=tag:container
TS_AUTH_ONCE: "true"
TS_HOSTNAME: ecs
TS_USERSPACE: "false"
# 禁用收集或发送任何日志数据,会发往 https://log.tailscale.io
TS_NO_LOGS_NO_SUPPORT: "true"
TS_SOCKET: "/var/run/tailscale/tailscaled.sock"
TS_STATE_DIR: /var/lib/tailscale/
#TS_LOGIN_SERVER: "http://127.0.0.1:528"
#TS_CONTROL_IS_PLAINTEXT_HTTP: "true"
TS_DEBUG_FIREWALL_MODE: nftables # 使用和宿主机模式一致的

derper:
container_name: derper
image: registry.aliyuncs.com/zhangguanzhang/derper:v1.70.0
restart: unless-stopped
network_mode: host
volumes:
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
# 容器生成的证书存放,如果是自己的绿锁证书,存放文件名为
# $DERP_DOMAIN.key $DERP_DOMAIN.crt
- ./cert:/cert
- /var/run/tailscale/:/var/run/tailscale/
environment:
DERP_DOMAIN: my.mydomain.no # 域名,由于不是绿锁 https,随意写,和后面 derp.yaml 一致即可
DERP_ADDR: ':12345' # https 端口
DERP_STUN_PORT: '3478' # udp port
DERP_HTTP_PORT: '-1'
DERP_VERIFY_CLIENTS: "true"
DERP_CERT_DIR: /cert
depends_on:
- tailscale
EOF
1
docker-compose up -d

客户端接入

所有客户端有微皮恩的,需要把 ecs 的公网 IP 设置成直连不走代理,以及如果是家里宽带,可以把 Upnp 打开,这样打洞直连成功率会高些。

创建 authkeys

Tailscale 中有一个概念叫 tailnet,你可以理解成租户,租户与租户之间是相互隔离的,具体看参考 Tailscale 的官方文档: What is a tailnet
Headscale 也有类似的实现叫 user,即用户。我们需要先创建一个 user,以便后续客户端接入,例如:

1
headscale user create default

其他客户端接入需要首先在服务端生成 pre-authkey 的 key :

1
2
3
4
5
6
7
# 生成一个过期时间 365d 且可以重复使用的 authkey
$ headscale preauthkeys --user default create --reusable --expiration 365d

# 查看已经生成的 key:
$ headscale preauthkeys --user default list
ID | Key | Reusable | Ephemeral | Used | Expiration | Created | Tags
1 | 49f9cd7f4e7b3e33023a9064xxxxxebf00778d2xxxxxxxxx | false | false | false | 2025-07-24 14:32:45 | 2024-07-24 14:32:45 |

tailscale 也是分为 tailscaled 的 daemon 和 tailscale 的 cli 工具,windows、Linux 以及安卓的 Magisk 模块等都可以使用 cli 工具操作和排查,这点很重要。

下面是 tailscale up 时候一些常用通用选项:

  • --login-server: 指定使用的中央服务器地址(必填)
  • --advertise-routes: 向中央服务器报告当前客户端处于哪个内网网段下, 便于中央服务器让同内网设备直接内网直连(可选的)或者将其他设备指定流量路由到当前内网(可选),多条路由英文逗号隔开
  • --accept-routes: 是否接受中央服务器下发的用于路由到其他客户端内网的路由规则(可选)
  • --accept-dns: 是否使用中央服务器下发的 DNS 相关配置(可选, 推荐关闭)
  • --hostname: 设置 machine name,否则默认会以 hostname 注册上去,特别安卓的 hostname 无法修改

tailscale cli 官方文档 https://tailscale.com/kb/1080/cli,也可以自己 tailscale --help 看命令帮助。

Linux 接入

derp 上的客户端

先接入 derp 也就是 ecs 上那个 tailscale:

1
2
3
4
5
# 也可以拷贝出来宿主机上执行
# docker cp tailscale:/usr/local/bin/tailscale /usr/bin/tailscale
docker exec -ti tailscale sh

tailscale up --login-server=http://<HEADSCALE_PUB_ENDPOINT>:8081 --accept-routes=true --hostname ecs --accept-dns=false --authkey 49f9cd....

可以 headscale 上查看信息:

1
2
3
$ headscale node list
ID | Hostname | Name | MachineKey | NodeKey | User | IP addresses | Ephemeral | Last seen | Expiration | Online | Expired
1 | ecs | ecs | [3ixoT] | [VFZTB] | default | 100.63.0.1, | false | 2025-01-25 05:20:50 | 0001-01-01 00:00:00 | online | no

Linux 上 tailscale 会利用 tun 创建网卡,路由表在 52 里:

1
$ ip route show table 52
Linux 客户端

官方相关文档:

1
curl -fsSL https://tailscale.com/install.sh | sh

然后自己 tailscale up --...... 登录。

windows

https://tailscale.com/download 下载安装(安装失败换 msi 安装包 https://pkgs.tailscale.com/stable/#windows ),如果 msi 安装包启动报错 iphelp 啥的相关,在 services.msc 里把 ip helper 属性设置自启动后启动下。

启动后,官方文档 windows-client 说需要 powershell 修改下面的注册表:

1
2
3
New-Item -Path "HKLM:\SOFTWARE\Tailscale IPN"
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://YOUR-HEADSCALE-URL:8081

但是没必要,而且系统托盘图标点击 login 可能无法弹出浏览器页面,可以 powershell 或者 gitbash 里执行 tailscale cli 注册:

1
tailscale up --login-server http://xxx:8081 --hostname laptop --accept-routes=true --accept-dns=false --authkey 49f9cd....

如果执行后卡住的,在 services.msc 里把 tailscale 重启下后再试试。

windows 开启 --unattended 锁屏不会断开连接,一些 windows 早期版本问题见下面 issue :

windows 关闭上传日志到 log.tailscale.io 可以在 C:\ProgramData\Tailscale 下新增 tailscaled-env.txt 写入 env:

1
TS_NO_LOGS_NO_SUPPORT=true

安卓

https://github.com/tailscale/tailscale-android

推荐 https://f-droid.org/packages/com.tailscale.ipn/ 下载,请下载 1.72.0 ,如果不是 https url 不要尝试使用 v1.76 <= x < v1.80 我尝试开发版本 1.78.1-t89039 版本无法注册,见原因 https://github.com/tailscale/tailscale/issues/13794

安装后在 右上角 - Accounts - 三个点 - Settings Accounts Use an alternate server,输入 http://xxx:8081,然后下面 Use an auth key 可能会没使用 authkey 跳转到浏览器出现一个 headscale nodes register ... --key nodekey:xxxx ,所以我们需要回到 headscale 上命令授权:

1
2
3
4
# 日志里会打印长串 key,所以不需要在其他端复制 nodekey
journalctl -xe --no-pager -u headscale | grep key

headscale nodes register --user default --key mkey:xxxxx

对于一些没有浏览器也没 tailscale cli 的都可以这样手动授权下。安卓上点击每个 peer 进去的右上角图标等于 tailscale ping xxx ,会显示能否直连和延迟。也可以后续使用 Magisk tailscale,那样可以有 cli 了,另外 apk 是使用 V-P-N 形式,断网和切换流量会断开,而 Magisk tailscale 则不会。

adb 启动

1
adb shell am start -n com.tailscale.ipn/com.tailscale.ipn.MainActivity

openwrt

参照 https://github.com/adyanth/openwrt-tailscale-enabler ,需要有 kmod-tun 模块包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
VER=1.74.1
wget https://pkgs.tailscale.com/stable/tailscale_${VER}_arm64.tgz
tar zxvf tailscale_*_arm64.tgz
mkdir -p /etc/tailscale/
mv tailscale_*_arm64/tailscal* /usr/bin
rm -rf tailscale_*_arm64

cat << EOF > /etc/init.d/tailscale
#!/bin/sh /etc/rc.common

# Copyright 2020 Google LLC.
# SPDX-License-Identifier: Apache-2.0

USE_PROCD=1
START=99
STOP=1

start_service() {
procd_open_instance
procd_set_param command /usr/bin/tailscaled

# Set the port to listen on for incoming VPN packets.
# Remote nodes will automatically be informed about the new port number,
# but you might want to configure this in order to set external firewall
# settings.
# procd_append_param command --port 41641

# OpenWRT /var is a symlink to /tmp, so write persistent state elsewhere.
procd_append_param command --state /etc/config/tailscaled.state

# Persist files for TLS cert & Taildrop files
procd_append_param command --statedir /etc/tailscale/
procd_append_param command -no-logs-no-support

procd_set_param env TS_DEBUG_FIREWALL_MODE=iptables

procd_set_param respawn
procd_set_param stdout 1
procd_set_param stderr 1

procd_close_instance
}

stop_service() {
/usr/bin/tailscaled --cleanup
}
EOF

chmod a+x /etc/init.d/tailscale
/etc/init.d/tailscale enable
/etc/init.d/tailscale start

tailscale status

登录

1
2
3
tailscale up --login-server=http://<HEADSCALE_PUB_ENDPOINT>:8081 --accept-routes=true --hostname openwrt --accept-dns=false --authkey 49f9cd....

tailscale status

查看日志:

1
logread tailscaled

主路由不设置路由下,转发放行+ snat:

1
iptables -I ts-forward -i br-lan -o tailscale0 -j MARK --set-xmark 0x40000/0xff0000

tailscale client 使用

每个 tailscale 端都可以执行命令来查看和排查一些信息。

1
2
3
# debug 命令隐藏在 --help 了,可以 tailscale debug --help 自行查看
# 打印 derp-map 信息
tailscale debug derp-map

查看和检测当前网络,会输出当前 derp 服务器信息:

1
2
3
4
5
6
7
8
9
10
11
$ tailscale netcheck

Report:
* UDP: true
* IPv4: yes, xx.x.xx.xxx:54417
* IPv6: no, but OS has support
* MappingVariesByDestIP:
* PortMapping:
* Nearest DERP: tx
* DERP latency:
- custom: 500µs (tx)

tailscale ping 命令可以用于测试 IP 连通性, 同时可以看到时如何连接目标节点的. 默认情况下 Ping 命令首先会使用 Derper 中继节点通信, 然后尝试 P2P 连接; 一旦 P2P 连接成功则自动停止 Ping:

1
2
3
$ tailscale ping 100.63.0.3
pong from redmi8 (100.63.0.3) via DERP(custom) in 30ms
pong from redmi8 (100.63.0.3) via 192.168.0.107:47316 in 32ms

status 查看以及 peer 的信息:

1
2
tailscale status
tailscale status --json

修改当前节点信息,支持修改的属性 –help 自行查看:

1
2
# tailscale set --help
tailscale set --hostname=xxx

或者可以 down 后 up 带单一需要修改的参数执行下:

1
2
3
tailscale down
tailscale up --xxx
# 然后会打印全部参数,复制执行下

查看当前的参数列表,官方没有 cmdline 形式打印,只有 json 的:

1
tailscale debug prefs

打通内网

Linux 端都要开启转发,windows 和安卓转发自行查找怎么配置。

1
2
3
echo 'net.ipv4.ip_forward = 1' | tee /etc/sysctl.d/ipforwarding.conf
echo 'net.ipv6.conf.all.forwarding = 1' | tee -a /etc/sysctl.d/ipforwarding.conf
sysctl -p /etc/sysctl.d/ipforwarding.conf

然后在 server 端查看 node ID :

1
2
3
4
5
$ headscale node list
ID | Hostname | Name | MachineKey | NodeKey | User | IP addresses | Ephemeral | Last seen | Expiration | Online | Expired
1 | ecs | ecs | [3ixoT] | [VFZTB] | default | 100.63.0.1, | false | 2025-01-26 07:12:50 | 0001-01-01 00:00:00 | online | no
2 | localhost | ax18 | [egfcx] | [LzS5J] | default | 100.63.0.2, | false | 2025-01-26 07:13:14 | 0001-01-01 00:00:00 | online | no
3 | localhost | redmi8 | [uqIVP] | [l6sL7] | default | 100.63.0.3, | false | 2025-01-26 07:13:06 | 0001-01-01 00:00:00 | online | no

假设 ID==1 的局域网是 192.168.31.0/24 网段,我们希望其他 ID 设备上能访问到,先查看路由:

1
2
3
4
$ headscale routes list
ID | Machine | Prefix | Advertised | Enabled | Primary
.....
1 | ax18 | 192.168.31.0/24 | true | false | false
1
headscale routes enable -r 1

其他节点查看路由结果:

1
2
$ ip route show table 52 | grep "192.168.31.0/24"
192.168.31.0/24 dev tailscale0

其他节点启动时需要增加 --accept-routes=true 选项来声明 “我接受外部其他节点发布的路由”。

现在你在任何一个 Tailscale 客户端所在的节点都可以 ping 通家庭内网的机器了,你在公司或者星巴克也可以像在家里一样用同样的 IP 随意访问家中的任何一个设备。

一个正在运行的节点增加路由可以使用 set 命令:

1
2
# 多条用英文逗号间隔
tailscale set --advertise-routes xx.xx.xx.0/24,xx,xxx.xxx.00.00/16

信息修改

修改 node 的 name,使用

1
headscale nodes rename -h

因为默认 db 使用 sqlite,可以修改一些信息:

1
2
3
4
5
$ sqlite3 /var/lib/headscale/db.sqlite
.tables

# ip_addresses="100.63.0.3"
修改 ip 需要先 tailscale down 改好后再 tailscale up,修改后配置会同步到所有 tailscale client 上,特别是游戏可能会闪断下

其他的修改自己琢磨。

一些说明

这里只介绍异地组网部分,其他的去看官方文档。

参考

请有自己的理解能力,不要随便照抄,端口啥的也可以自定义下。

CATALOG
  1. 1. 由来
  2. 2. 为什么我需要 headscale
  3. 3. 部署
    1. 3.1. 历史版本
    2. 3.2. headscale 部署和设置
    3. 3.3. derp 部署
    4. 3.4. 客户端接入
      1. 3.4.1. 创建 authkeys
      2. 3.4.2. Linux 接入
        1. 3.4.2.1. derp 上的客户端
        2. 3.4.2.2. Linux 客户端
      3. 3.4.3. windows
      4. 3.4.4. 安卓
      5. 3.4.5. openwrt
    5. 3.5. tailscale client 使用
    6. 3.6. 打通内网
    7. 3.7. 信息修改
    8. 3.8. 一些说明
  4. 4. 参考