点击展开更新日志

2026

title

xxxxx

nexttime

会有些什么呢(❁´◡`❁)

源起

其实前两天把 OpenVPN 和 WireGuard 跑通之后就准备直接用的,但是依照现有配置无法实现 P2P 直连,必需经过公网服务器中转,还是觉得不太满意,所以就准备再试试 tailscale/headscale 的方案。

tailscale 是基于 WireGuard 的方案,如果客户端满足直连条件,会优先使用 P2P,否则会降级使用中继服务器,本身是商业软件,不过对于个人用户,免费方案足够用了;headscale 是开源实现,功能上和官方版本基本一致,用户客户端也是官方的。

服务端部署

采用 Docker 部署,方便部署。

安装Docker

忽略。

买的腾讯云,直接就是带 Docker 的镜像,也自带了镜像加速,建议。国内的服务器厂商一般也都提供的自家服务器的加速镜像,可以查一下文档。

创建数据目录

1
2
3
4
5
6
7
sudo mkdir -p /data/docker/headscale/{headplane, headscale, traefik}
# headplane: 可视化 Web ui
sudo mkdir -p /data/docker/headscale/headplane/data
# headscale
sudo mkdir -p /data/docker/headscale/headscale/{config, lib, run}
# traefik 服务发现注册,也可以用Nginx或NPM
sudo mkdir -p /data/docker/headscale/traefik/{acme, certs, dynamic}

准备配置

headplane/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
server:
host: "0.0.0.0"
port: 3000
base_url: "http://localhost:3000"
cookie_secret: "<SECRET_KEY>"
cookie_secure: true
cookie_max_age: 86400
data_path: "/var/lib/headplane"
headscale:
url: "http://headscale:8080"
config_path: "/etc/headscale/config.yaml"
config_strict: true
integration:
agent:
enabled: false
pre_authkey: "<your-preauth-key>"
docker:
enabled: false
container_label: "me.tale.headplane.target=headscale"
socket: "unix:///var/run/docker.sock"
kubernetes:
enabled: false
validate_manifest: true
pod_name: "headscale"

proc:
enabled: false

cookie_secret 可以使用 openssl rand -hex 16 生成准确32位的字符串,使用 openssl rand -base64 32 生成的字符串经 base64 编码会超出32位导致启动失败。

headscale/config/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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
---
server_url: https://headscale.domain.top
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 0.0.0.0: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: 100.64.0.0/10
v6: fd7a:115c:a1e0::/48
allocation: sequential
derp:
server:
enabled: true
region_id: 999 自定义唯一区域ID,建议901-999
region_code: "Tencent" # 自定义区域代码
region_name: "Tencent lighthouse in Guangzhou" # 区域描述
verify_clients: true
stun_listen_addr: "0.0.0.0:3478" # STUN服务UDP端口,NAT穿透发现
private_key_path: /var/lib/headscale/derp_server_private.key # 密钥,自动生成
automatically_add_embedded_derp_region: true # 自动将此DERP加入服务器列表
ipv4: <IPv4>
ipv6: <IPv6>
urls:
- https://controlplane.tailscale.com/derpmap/default
paths: []
auto_update_enabled: true
update_frequency: 3h
disable_check_updates: false
ephemeral_node_inactivity_timeout: 30m

database:
type: sqlite
debug: false
gorm:
prepare_stmt: true
parameterized_queries: true
skip_err_record_not_found: true
slow_threshold: 1000

# SQLite config
sqlite:
path: /var/lib/headscale/db.sqlite
write_ahead_log: true
wal_autocheckpoint: 1000
acme_url: https://acme-v02.api.letsencrypt.org/directory
acme_email: ""
tls_letsencrypt_hostname: ""
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
tls_letsencrypt_challenge_type: HTTP-01
tls_letsencrypt_listen: ":http"
tls_cert_path: ""
tls_key_path: ""

log:
level: info
format: text
policy:
mode: file
path: ""

dns:
magic_dns: true
base_domain: clients.domain.top
override_local_dns: true
nameservers:
global:
- 1.1.1.1
- 8.8.8.8
split: {}
search_domains: []
extra_records: []
unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"
logtail:
enabled: false
randomize_client_port: false
taildrop:
enabled: true

根据实际情况修改 server_urlbase_url,补充DERP服务器的公网地址。

  • server_url: 访问headscale的yum
  • base_url: 生成客户端域名地址

证书

将域名证书上传到 traefik/certs 目录下,traefik 可以直接配置 Let’s Encrypt 证书自动申请,只是我已经在 allinssl 中配置了申请,所以第一次直接配置了,后续就用 allinssl 做更新了。

最终完成的结构目录如下:

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
/data
└── docker
└── headscale
├── docker-compose.yml
├── headplane
│ ├── config.yaml
│ └── data
│ └── hp_persist.db
├── headscale
│ ├── config
│ │ ├── config.yaml
│ │ └── dns_records.json
│ ├── lib
│ │ ├── db.sqlite
│ │ ├── db.sqlite-shm
│ │ ├── db.sqlite-wal
│ │ ├── derp_server_private.key
│ │ └── noise_private.key
│ └── run
│ └── headscale.sock
└── traefik
├── acme
│ └── acme.json
├── certs
│ ├── domain.top.crt
│ └── domain.top.key
├── dynamic
│ └── certificates.yml
└── traefik.yml

开放端口

开放以下端口:

  • 443/tcp
  • 3478/udp

服务部署

docker-compose.yml

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
services:
headplane:
image: ghcr.io/tale/headplane:latest
container_name: headplane
restart: unless-stopped
environment:
- TZ=Asia/Shanghai
# ports:
# - '3000:3000'
volumes:
- './headplane/config.yaml:/etc/headplane/config.yaml'
- './headplane/data:/var/lib/headplane'
- './headscale/config/config.yaml:/etc/headscale/config.yaml'
# - './headscale/dns_records.json:/etc/headscale/dns_records.json'
- '/var/run/docker.sock:/var/run/docker.sock:ro'
labels:
# Expose the admin UI at /admin
- 'traefik.enable=true'
- 'traefik.http.routers.headplane.rule=Host(`headscale.domain.top`) && PathPrefix(`/admin`)'
- 'traefik.http.routers.headplane.entrypoints=websecure'
- 'traefik.http.routers.headplane.tls=true'
- 'traefik.http.routers.headplane.priority=100'
- 'traefik.http.routers.headplane.service=headplane-service'
- 'traefik.http.services.headplane-service.loadbalancer.server.port=3000'
networks:
- headscale

headscale:
image: headscale/headscale:latest
container_name: headscale
restart: unless-stopped
command: serve
environment:
- TZ=Asia/Shanghai
ports:
# - '8080:8080'
# - '9090'
# - '3478:3478/tcp'
- '3478:3478/udp'
# - '9090:9090'
volumes:
- './headscale/config:/etc/headscale'
- './headscale/lib:/var/lib/headscale'
- './headscale/run:/var/run/headscale'
healthcheck:
test: ["CMD", "headscale", "health"]
labels:
- 'me.tale.headplane.target=headscale'

# Traefik labels to expose Headscale at headscale.example.com
- 'traefik.enable=true'
- 'traefik.http.routers.headscale.rule=Host(`headscale.domain.top`) && !PathPrefix(`/admin`)'
- 'traefik.http.routers.headscale.entrypoints=websecure'
- 'traefik.http.routers.headscale.tls=true'
- 'traefik.http.routers.headscale.service=headscale-service'
- 'traefik.http.routers.headscale.priority=10'
# 服务定义
- 'traefik.http.services.headscale-service.loadbalancer.server.port=8080'

# This middleware is essential to ensuring Headplane works correctly
- 'traefik.http.routers.headscale.middlewares=cors'
- 'traefik.http.middlewares.cors.headers.accesscontrolallowheaders=*'
- 'traefik.http.middlewares.cors.headers.accesscontrolallowmethods=GET,POST,PUT'
- 'traefik.http.middlewares.cors.headers.accesscontrolalloworiginlist=https://headscale.domain.top'
- 'traefik.http.middlewares.cors.headers.accesscontrolmaxage=100'
- 'traefik.http.middlewares.cors.headers.addvaryheader=true'

# If you would optionally like to automatically redirect / to /admin
- 'traefik.http.routers.headscale-root.rule=Host(`headscale.domain.top`) && Path(`/`)'
- 'traefik.http.routers.headscale-root.entrypoints=websecure'
- 'traefik.http.routers.headscale-root.tls=true'
- 'traefik.http.routers.headscale-root.service=headscale-service'
- 'traefik.http.routers.headscale-root.priority=50'
- 'traefik.http.routers.headscale-root.middlewares=rewrite-root,cors'
- 'traefik.http.middlewares.rewrite-root.redirectregex.regex=^/$$'
- 'traefik.http.middlewares.rewrite-root.redirectregex.replacement=/admin'
- 'traefik.http.middlewares.rewrite-root.redirectregex.permanent=true'
networks:
- headscale

traefik:
image: traefik:latest
container_name: traefik
restart: unless-stopped
ports:
- '80:80'
- '443:443'
# - '8080'
volumes:
# Example volumes/setup, please configure Traefik as needed
- './traefik/traefik.yml:/etc/traefik/traefik.yml'
- './traefik/dynamic:/etc/traefik/dynamic:ro'
- '/var/run/docker.sock:/var/run/docker.sock:ro'
- './traefik/certs:/certs'
- './traefik/acme:/etc/traefik/acme'
- '/etc/localtime:/etc/localtime:ro'
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`traefik.domain.top`)"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.service=api@internal"
- "traefik.http.routers.traefik.tls=true"
# Basic Auth 中间件(保护 Dashboard)
- "traefik.http.routers.traefik.middlewares=auth"
- "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$9IGx0e5f$$7g1ShFdT7B/eFRjrOpzZb1"
# 可选:添加 security headers 中间件
- "traefik.http.middlewares.security-headers.headers.browserXSSFilter=true"
- "traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true"
- "traefik.http.middlewares.security-headers.headers.frameDeny=true"
- "traefik.http.middlewares.security-headers.headers.sslRedirect=true"
networks:
- headscale
networks:
headscale:
name: headscale

traefik 的 Basic Auth 认证的账号密码,使用以下方法生成进行替换:

1
2
sudo apt install apache2-utils
htpasswd -nb admin <your-password> | sed -e s/\\$/\\$\\$/g

最后启动容器服务即可,检查日志是否存在报错,没有报错就可以继续了。也可以访问 https://headscale.domain.top/health 查看返回结果:

1
2
3
{
"status": "pass"
}

管理地址

headplane 的 Web ui管理地址:https://headscale.domain.top/admin

headscale 服务地址: https://headscale.domain.top

客户端配置

如果到这里一切顺利,服务正常启动,那么就可以开始客户端的配置了。

  1. Tailscale 下载页 下载客户端;
  2. 生成API Key:登录服务器执行:docker exec headscale headscale apikeys create 生成API Key
    1. 选择一:不写入配置,可以一直使用,如果过期/忘记重新生成即可;
    2. 选择二:写入配置,如果过期/忘记重新生成后需要修改配置重新部署;
  3. 进入 headplane 管理后台,使用 API Key登录,进入【User】标签新增一个用户,记住用户名;
  4. 以 Win11 为例,打开【终端】,添加客户端:
1
tailscale up --login-server=https://headscale.domain.top
  • --login-server:指定控制服务器地址(必须和你设置的 server_url 一致)。
  • `–
  1. 输入之后会弹出一个URL地址,在浏览器打开,会显示一条命令要求到服务端注册客户端:
1
docker exec headscale headscale nodes register --key <KEY> --user <USERNAME>

用户名就是上面手动添加的用户名。

  1. 服务器上添加完成后,终端内会显示【Success】说明客户端已添加成功,可以到 headplane后台查看了,后续添加多个客户端如果互访就使用分配的100.64.0.0/10段的IP即可。

tailscale 的一个优势是组建的内网是专用网络,因此网络权限很大,不需要像 Wireguard 需要额外开放策略存在安全风险。