Headscale 内网穿透方案

概述

Tailscale 基于用户态 WireGuard 协议构建,通过三层穿透策略实现高效组网,在所有节点间流量通过 WireGuard 实现高性能的端到端加密隧道。但是控制端是闭源的,子网无法自定义,免费版只能使用 20 台设备。这些都能忍受,因为个人使用完全足以。最大的缺点是官方中继 DERP 服务器全都在海外,之前个人使用时,还能稳定在 50ms 左右,但是最近延迟突然变得很大,于是需要找到了开源替代解决方案——Headscale。

Headscale 是 Tailscale 的开源控制平面,复刻 Tailscale 控制服务器逻辑,支持无限设备、私有化部署、自定义子网段。非常适合个人使用或小型开源组织。

DERP 是 Tailscale 自研的一个协议,但是官方中继 DERP 服务器全都在海外,为了实现低延迟、高安全性,我们可以参考 Tailscale 官方文档自建私有的 DERP 服务器。官方提供的部署方法,默认仅支持基于域名访问,对于国内的服务器,域名需要备案。

本文会介绍如何自建 Headsale 实现控制平面,并基于公有云服务器实现自定义 DERP 服务器。

先决条件

  • 带有公网 IP 的主机

  • 可以公网解析的域名

  • 权威证书

headscale 安装

常规安装方法

headscale

该方法需要有可以公网解析的域名,同时如果服务器在国内的话,域名需要备案。

  1. 从官方 Release 页面,根据你的操作系统选择对应的安装包,例如我的是 Ubuntu 系统,所以下载 headscale_0.26.1_linux_amd64.deb 这个包。
1
2
wget https://github.com/juanfont/headscale/releases/download/v0.26.1/headscale_0.26.1_linux_amd64.deb -O headscale.deb
apt install ./headscale.deb

通过这种方式,可以自动初始化 config.yamlheadscale.service这两个配置文件。并自动创建对应的用户和用户组。

  1. 配置/etc/headscale/config.yaml ,这里需要关注几个配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
## 访问的域名
server_url: https://headscale.zerchin.xyz

## API 监听端口
listen_addr: 0.0.0.0:8080

##
grpc_listen_addr: 0.0.0.0:50443

## 根据查询到的资料,建议打开随机端口,否则可能会出现连接不稳定的问题
randomize_client_port: true

## 自定义子网,如果跟现有网段不冲突,可以不修改
prefixes:
v4: 100.64.0.0/10
  1. 启动 headscale 服务
1
systemctl enable --now headscale
  1. 检查 headscale 服务状态
1
2
systemctl status headscale
journalctl -xef -u headscale

headscale-ui

默认 headscale 不提供 Web UI 界面,但是社区提供了很多实现 UI 的工具,例如这里可以使用 Headscale-ui 为 headscale 提供 Web UI 展示。

需要安装 docker 环境

1
docker run -itd -p 3000:8080 --name headscale-ui   ghcr.io/gurucomputing/headscale-ui:latest

安装好之后,访问 http://<your_IP>:3000/web

如果使用的是 headscale-admin,则访问的地址是 /admin

此时 UI 还对接不上 headscale,会有跨域问题,所以要做个反向代理把他俩串起来。

nginx

社区有很多种反向代理的实现方案,例如使用 caddy、traefik、nginx 等实现反向代理。这里使用我最熟悉的 nginx 来实现。

具体参考配置如下:

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
worker_processes auto;
worker_rlimit_nofile 40000;

events {
worker_connections 8192;
}

http {

map $http_upgrade $connection_upgrade {
default Upgrade;
'' close;
}

server {
listen 443 ssl http2;
server_name headscale.zerchin.xyz;

ssl_certificate /etc/nginx/ssl/tls.crt;
ssl_certificate_key /etc/nginx/ssl/tls.key;
ssl_protocols TLSv1.2 TLSv1.3;


location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $server_name;
proxy_redirect http:// https://;
proxy_buffering off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
}

location /web/ {
proxy_pass http://127.0.0.1:3000/web;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $server_name;
proxy_redirect http:// https://;
proxy_buffering off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
}
}
server {
listen 80;
server_name headscale.zerchin.xyz;
return 301 https://$host$request_uri;
}
}

这里需要修改这几个地方:

  • server_name:修改为访问的域名名称,不要带 http/https。
  • proxy_pass:这里填写 headscale 和 headscale-ui 的地址,如果前面按照我的步骤操作,则不用修改。

将上述文件保存到/root/headscale/nginx/路径下。

接着我们要准备好域名的权威证书,并将域名的权威证书放到 /root/headscale/nginx/ssl路径下,并重命名为 tls.crttls.key

最后通过 docker 启动 nginx 服务。

1
2
3
4
5
6
docker run -itd \
--name headscale-proxy \
-p 80:80 -p 443:443 \
-v /root/headscale/nginx/ssl:/etc/nginx/ssl \
-v /root/headscale/nginx/nginx.conf:/etc/nginx/nginx.conf \
nginx

验证

首先我们需要创建一个 key

1
headscale apikeys create

接着访问 UI:http://<your_domain>/web,在 Settings下填写配置。

其中:

  • Headscale URL 填写 https://<your_domain>
  • Headscale API Key 填写 前面刚刚创建的 apikey

接着点击 Test Server Settings,当旁边出现绿色的时,说明对接成功,我们可以通过 UI 管理 User、Device。

Docker Compose 安装方法

前面我们在安装 headscale-ui 和 nginx 时,我们使用了 docker 去启动。实际上,headscale 本身也有 docker 镜像,所以我们可以直接通过 docker compose 一键启动所有的服务。

  1. 首先新建一个存放配置的目录
1
2
3
4
cd ~
mkdir headscale
cd headscale
mkdir -p {nginx,nginx/ssl,config,lib,run}
  1. 接着在 nginx 目录下,配置 nginx.conf文件,在 nginx/ssl 目录下,存放证书文件,如下:
1
2
3
4
5
# ls nginx/*
nginx/nginx.conf

nginx/ssl:
tls.crt tls.key

nginx.conf 参考写法稍微有些不同,直接复制下面这个配置,修改一下 server_name 即可使用:

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
worker_processes auto;
worker_rlimit_nofile 40000;

events {
worker_connections 8192;
}

http {

map $http_upgrade $connection_upgrade {
default Upgrade;
'' close;
}

server {
listen 443 ssl http2;
server_name headscale.zerchin.xyz;

ssl_certificate /etc/nginx/ssl/tls.crt;
ssl_certificate_key /etc/nginx/ssl/tls.key;
ssl_protocols TLSv1.2 TLSv1.3;

location / {
proxy_pass http://headscale:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $server_name;
proxy_redirect http:// https://;
proxy_buffering off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
}

location /web/ {
proxy_pass http://headscale-ui:8080/web;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $server_name;
proxy_redirect http:// https://;
proxy_buffering off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
}
}
server {
listen 80;
server_name headscale.zerchin.xyz;
return 301 https://$host$request_uri;
}
}

配置 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
services:
headscale:
image: docker.io/headscale/headscale:v0.26.1-debug
restart: unless-stopped
container_name: headscale
ports:
- "8080:8080"
- "9090:9090"
volumes:
- ./config:/etc/headscale
- ./lib:/var/lib/headscale
- ./run:/var/run/headscale
command: serve
headscale-ui:
image: ghcr.io/gurucomputing/headscale-ui:latest
restart: unless-stopped
container_name: headscale-ui
ports:
- "3000:8080"
headscale-proxy:
image: nginx
restart: unless-stopped
container_name: headscale-proxy
ports:
- "443:443"
- "80:80"
volumes:
- "./nginx/nginx.conf:/etc/nginx/nginx.conf"
- "./nginx/ssl:/etc/nginx/ssl"
depends_on:
- headscale
- headscale-ui

这里 headsacle 使用了-debug的镜像,会自带一些基础工具,否则连 sh、ls 这些命令都没有

最后直接一键启动。

1
docker-compose up -d

安装好之后,apikey 通过 docker 命令获取。

1
docker exec -it headscale headscale apikey create 

http 安装方法

如果没有域名,则无法通过上述方法进行安装,此时我们可以使用 http 方式安装。

需要修改这几个地方:

  1. headscale 的 config.yaml 文件里,修改 server_url: http://xx.xx.xx.xx
  2. nginx.conf 配置需要进行修改,参考如下,可以直接修改其中的 server_name 为自己的 IP 直接使用:
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
worker_processes auto;
worker_rlimit_nofile 40000;

events {
worker_connections 8192;
}

http {

map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

server {
listen 80;
server_name 192.168.2.51;

location / {
proxy_pass http://headscale:8080;

proxy_set_header Host $server_name;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;

proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffer_size 64k;
proxy_buffers 16 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
}
location /web/ {
proxy_pass http://headscale-ui:8080/web;

proxy_set_header Host $server_name;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;

proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffer_size 64k;
proxy_buffers 16 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
}
}
}
  1. docker-compose.yml 中把 tls 相关的配置拿掉
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
services:
headscale:
image: docker.io/headscale/headscale:v0.26.1-debug
restart: unless-stopped
container_name: headscale
ports:
- "8080:8080"
- "9090:9090"
volumes:
- ./config:/etc/headscale
- ./lib:/var/lib/headscale
- ./run:/var/run/headscale
command: serve
headscale-ui:
image: ghcr.io/gurucomputing/headscale-ui:latest
restart: unless-stopped
container_name: headscale-ui
ports:
- "3000:8080"
headscale-proxy:
image: nginx
restart: unless-stopped
container_name: headscale-proxy
ports:
- "80:80"
volumes:
- "./nginx/nginx.conf:/etc/nginx/nginx.conf"
depends_on:
- headscale
- headscale-ui

最后执行docker-compose up -d启动,效果如下:

创建用户

在 Headscale 中,一个用户管理着多个节点(设备),用户与用户之间是隔离的。节点始终只会归属于一个用户,在一个用户下的节点网络是互通的。

首先我们需要创建一个用户:

1
headscale user create user01

创建 preauthkeys,在 Linux 客户端需要用到,保存备用:

1
headscale preauthkeys create -u 1 --reusable -e 1d

这里官方文档说明中,-u 后面跟的是 user name,但是实际上需要填写 user id

user 和 preauthkeys 也可以通过 UI 创建。

客户端节点注册

客户端是使用原生的 tailscale 工具。

目前几乎所有的系统都能支持运行,包括 主流 Linux 操作系统,Windows、Mac、IOS、Android。

这里以 Ubuntu 和 Mac 为例。其他系统也是类似,具体细节可以参考 headscale 官方文档

Ubuntu

配置必要的内核参数:

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

首先要安装 tailscale 工具,官方提供了一键安装的脚本,我们可以直接执行:

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

这条命令会安装 tailscale 工具,并启动 tailscaled 服务。安装好之后,执行命令确认该服务是否是 running 状态。

1
systemctl status tailscaled

确认 tailscale0 网络设备已创建出来。

1
2
3
4
5
# ip addr show tailscale0
3: tailscale0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1280 qdisc fq_codel state UNKNOWN group default qlen 500
link/none
inet6 fe80::d06a:c117:207f:648f/64 scope link stable-privacy
valid_lft forever preferred_lft forever

接下来就可以通过 tailscale 接入到 headscale。

方法1:(Ubuntu 请直接参考方法 2,其他系统可以参考此方法)

直接在节点上执行注册命令:

1
tailscale up --login-server https://headscale.zerchin.xyz --accept-dns=false --accept-routes --advertise-routes 192.168.2.0/24

然后会输出一个地址得到个 key,在 Server 上执行命令注册:

1
headscale nodes register --user <USER> --key <YOUR_MACHINE_KEY>

这个是官方给的第一个连接的方法,但是这里我试过有问题,所以直接看第二个方法。

方法 2:使用前面我们获取到的 preauthkey 注册节点:

1
tailscale up --login-server https://headscale.zerchin.xyz --authkey 7c6601397c9e0c06fc28743134e24bf6f851eee0cf65af88 --accept-dns=false --accept-routes=true --advertise-routes=192.168.2.0/24

主要参数说明:

  • authkey:已授权的 key,使用该 key 直接就能注册客户端节点,不需要 Headscale 手动同意。
  • accept-dns:是否接受 DNS 配置,默认是 true。
  • accept-routes:是否接受其他节点通告的路由,默认是 false。
  • advertise-routes:宣告给其他节点的路由,如果有多条,则以逗号分隔,例如"10.0.0.0/8,192.168.0.0/24"

查看节点列表,可以看到已经注册上来并且是 online 状态。

1
2
3
# headscale node list
ID | Hostname | Name | MachineKey | NodeKey | User | IP addresses | Ephemeral | Last seen | Expiration | Connected | Expired
1 | hs-31 | hs-31 | [1SQ6S] | [cP7Bw] | user01 | 100.64.0.1, fd7a:115c:a1e0::1 | false | 2025-06-08 08:08:43 | N/A | online | no

查看路由列表:

1
2
3
# headscale node list-routes
ID | Hostname | Approved | Available | Serving (Primary)
1 | hs-31 | | 192.168.2.0/24 |

这里路由只是告知给了 headscale,还需要通过 Headscale 手动同意宣告该路由才行。

1
2
# headscale node approve-routes -i 1 -r 192.168.2.0/24
Node updated

确认路由已被宣告:

1
2
3
# headscale node list-routes
ID | Hostname | Approved | Available | Serving (Primary)
1 | hs-31 | 192.168.2.0/24 | 192.168.2.0/24 | 192.168.2.0/24

Mac

按住 option 键点击 tailscale 图标,会出现 Debug 选项,选择 Add Account,填写 headscale Server 地址:https://headscale.zerchin.xyz

接着浏览器会弹出一个窗口,该窗口包含注册节点的命令(其实就是上一步的方法一),如下:

将这个命令复制下来,到 headscale 的主机上运行:

1
headscale nodes register --user user01 --key hobweC-xxgC-uzgjdZF4GkqI

这里 –user 要使用名字而不是 id

查看节点列表状态

1
2
3
4
# headscale node list
ID | Hostname | Name | MachineKey | NodeKey | User | IP addresses | Ephemeral | Last seen | Expiration | Connected | Expired
1 | hs-31 | hs-31 | [1SQ6S] | [cP7Bw] | user01 | 100.64.0.1, fd7a:115c:a1e0::1 | false | 2025-06-08 08:12:43 | N/A | online | no
2 | Zeqins-MacBook-Pro | zeqins-macbook-pro | [yvja1] | [blV3O] | user01 | 100.64.0.3, fd7a:115c:a1e0::3 | false | 2025-06-08 08:12:45 | N/A | online | no

DERP

DERP 全称是 Designated Encrypted Relay for Packets,是 Tailscale 网络中的一种加密中继服务器。DERP 中继仅充当“盲转发器”,不会解密流量,因为数据在设备端已全程加密,私钥不离设备。作用主要有两点:

  • 协助协商设备之间建立直接连接(NAT 穿透中的 “发现” 阶段);

  • 当直接连接不可行时,作为加密中继,将封装后的 WireGuard 数据包通过 HTTPS/TCP 转发。

Tailscale 在全球多个区域都部署了 DERP 服务器,客户端启动时会从控制端获取包含 DERP 服务器位置及延迟信息的 “DERP map”,并通过 netcheck 选择“家乡”节点,控制平面会把该信息告知其它设备。在实际连接中,设备 A 会先通过 B 正在使用的 DERP 中继尝试建立直接连接,如果成功则转为 UDP 直连,否则一直走中继流量。

但是 DERP 在中国大陆没有节点,所以可能会导致一旦流量通过 DERP 服务器进行中继,延时就会非常高。不过不用担心,官方提供了自建 DERP 服务器的方法。我们可以使用这种方法自建 DERP 服务器,可以基于域名 访问,也可以自行魔改使用 IP 访问。这两种方法各有利弊。

  • 域名:默认情况下 DERP 服务器可以被其他人使用,只要别人知道了你的 DERP 服务器的地址和端口,就会被白嫖。使用域名方式可以基于verify-clients参数开启验证,防止他人使用(同时需要 DERP 服务器上安装 Tailscale )。

  • IP:由于国内服务器+域名需要备案,此时就可以使用纯 IP 的方式进行访问,但是就无法开启验证了。IP 访问需要额外进行一些修改。

这里需要自行权衡利弊。下面会介绍如何启用这两种方法。

源码编译

  1. 准备 golang 环境
1
2
3
4
5
6
7
8
9
apt install -y wget git openssl curl
wget https://golang.google.cn/dl/go1.24.3.linux-amd64.tar.gz
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.24.3.linux-amd64.tar.gz

export PATH=$PATH:/usr/local/go/bin
go version

echo "export PATH=$PATH:/usr/local/go/bin" >> /etc/profile
source /etc/profile
  1. 下载 DERP 源代码
1
2
3
4
5
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
go install tailscale.com/cmd/derper@main

cp /root/go/bin/derper /usr/local/bin/derper
  1. 导入证书

创建证书目录,并将证书上传到这里,证书命名规则是<domian_name>.key<domian_name>.crt

1
mkdir -p /etc/derp
  1. 配置 systemd
1
2
3
4
5
6
7
8
9
10
11
12
cat /etc/systemd/system/derp.service
[Unit]
Description=TS Derper
After=network.target
Wants=network.target
[Service]
User=root
Restart=always
ExecStart=/usr/local/bin/derper -hostname <domain_name> -a :33445 -http-port 33446 -certmode manual -certdir /etc/derp
RestartPreventExitStatus=1
[Install]
WantedBy=multi-user.target
  1. 启动 derp 服务
1
systemctl enable --now derp
  1. 检查
1
journalctl -xef -u derp.service

防火墙需要放行 HTTPS 33445 和 STUN 3478 端口

纯 IP 方式

需要重新编译源码文件cert.go,编辑 文件,注释这个方法的前三行。

1
2
cd ~/go/pkg/mod/[tailscale]/cmd/derper
vi cert.go
1
2
3
4
5
6
7
8
9
10
11
12
func (m *manualCertManager) getCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
//if hi.ServerName != m.hostname && !m.noHostname {
// return nil, fmt.Errorf("cert mismatch with hostname: %q", hi.ServerName)
//}

// Return a shallow copy of the cert so the caller can append to its
// Certificate field.
certCopy := new(tls.Certificate)
*certCopy = *m.cert
certCopy.Certificate = certCopy.Certificate[:len(certCopy.Certificate):len(certCopy.Certificate)]
return certCopy, nil
}

重新编译:

1
go build -o /usr/local/bin/derper

准备自签名证书:

1
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes -keyout /etc/derp/derp.myself.com.key -out /etc/derp/derp.myself.com.crt -subj "/CN=derp.myself.com" -addext "subjectAltName=DNS:derp.myself.com"

设置 systemd:

1
2
3
4
5
6
7
8
9
10
11
12
cat /etc/systemd/system/derp.service
[Unit]
Description=TS Derper
After=network.target
Wants=network.target
[Service]
User=root
Restart=always
ExecStart=/usr/local/bin/derper -hostname derp.myself.com -a :33445 -http-port 33446 -certmode manual -certdir /etc/derp --verify-clients=false
RestartPreventExitStatus=1
[Install]
WantedBy=multi-user.target

启动 derp 服务:

1
systemctl enable --now derp

对接 Headscale

  1. 准备好 derp.json 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"Regions": {
"901": {
"RegionID": 901,
"RegionCode": "custom-derp-server",
"RegionName": "Custom Derp Server",
"Nodes": [
{
"Name": "901a",
"RegionID": 901,
"DERPPort": 33445,
"HostName": "xxxx",
"IPv4": "xxxx",
"InsecureForTests": true
}
]
}
}
}

主要参数说明:

  • RegionID :每个区域都有一个唯一的 ID。ID 值 900 到 999 保留用于自定义用户指定的区域,Tailscale 不会使用他们。
  • HostName:DERP 服务器的域名地址。
  • IPv4:如果是纯 IP 方式,则要填写 IP 地址。
  • InsecureForTests:跳过域名验证。

我们需要让 Headscale 读到这个配置文件,但是 Headscale 只能够通过 URL 的形式读取,所以我们可以自己弄一个 nginx 容器,参考如下:

1
2
3
4
docker run -itd --name derp-json -p 8081:80 -v ${PWD}/derp.json:/usr/share/nginx/html/derp.json nginx

## 验证是否能访问
curl localhost:8081/derp.json
  1. 接着编辑 headscale 的 config.yaml文件,将原本的官方的 derpmap 禁用掉,使用自己的 derp.json,修改位置如下:
1
2
3
4
derp:
urls:
#- https://controlplane.tailscale.com/derpmap/default
- http://<derp-json_server_ip>:8081/derp.json
  1. 最后重启一下 headscale 服务(如果是 docker 运行就重启容器)
1
2
3
systemctl restart headscale
## or
docker restart headscale
  1. 检查是否使用自己的 DERP 服务器。
1
2
3
4
5
6
7
# tailscale netcheck
...
...
...
* Nearest DERP: Custom Derp Server
* DERP latency:
- custom-derp-server: 2.8ms (Custom Derp Server)
  1. 查看延迟
1
2
# tailscale ping macbook-pro
pong from macbook-pro (100.64.198.12) via 27.38.171.20:35657 in 13ms

测了下速度,还是很可观的。

对接官方 Tailscale

自己准备的 DERP 服务器,也可以用于 Tailscale ,只需要在 Tailscale console - Access Control 下,添加如下配置:

如果是域名方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"derpMap": {
"OmitDefaultRegions": true,
"Regions": {
"900": {
"RegionID": 900,
"RegionCode": "custom-derp-server",
"RegionName": "Custom Derp Server",
"Nodes": [
{
"Name": "900a",
"RegionID": 900,
"DERPPort": 33445,
"HostName": "<your_domain_name>",
},
],
},
},
},

如果是纯 IP 方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"derpMap": {
"OmitDefaultRegions": true,
"Regions": {
"900": {
"RegionID": 900,
"RegionCode": "custom-derp-server",
"RegionName": "Custom Derp Server",
"Nodes": [
{
"Name": "900a",
"RegionID": 900,
"DERPPort": 33445,
"IPv4": "<ip_addr>",
"InsecureForTests": true,
},
],
},
},
},

主要参数说明:

  • OmitDefaultRegions:使用自己的 DERP 服务器而不使用官方的。

保存之后,在 Machines 页面下,点击任意一个客户端,可以看到是否选择了自定义的 DERP 服务器,看上去效果很不错。

构建 Docker 镜像

这里一步步安装还是太麻烦了点,那么我们是否可以自己构建 DERP 镜像,而不用每次都手动编译呢?答案是肯定的。

可以基于上述的步骤,自行构建 Docker 镜像,以后就可以直接在每个节点直接运行该镜像,快速拉起一个 DERP 服务。

这里我已经构建好镜像并测试验证通过,可放心食用。

1
docker run -itd --name derper --net host zerchin/derper-ip:v0.1

如果不放心的话,这里也放出来构建的过程,自行参考下面。

Dockerfile 文件参考如下:

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
FROM golang:latest AS builder

WORKDIR /app

RUN git clone https://github.com/tailscale/tailscale.git

RUN sed -i '/if hi\.ServerName != m\.hostname && !m\.noHostname {/,/}/ {
s/^/\/\//;
s/^/\/\//;
s/^/\/\//;
}' /app/tailscale/cmd/derper/cert.go

RUN cd /app/tailscale/cmd/derper/ && CGO_ENABLED=0 GOTOOLCHAIN=auto /usr/local/go/bin/go build -buildvcs=false -ldflags "-s -w" -o /app/derper

FROM ubuntu:24.04

WORKDIR /app

ENV DERP_ADDR :443
ENV DERP_HTTP_PORT 80
ENV DERP_HOST=127.0.0.1
ENV DERP_CERTS=/app/certs/
ENV DERP_STUN true
ENV DERP_VERIFY_CLIENTS false

RUN apt-get update && \
apt-get install -y openssl curl

COPY --from=builder /app/derper /app/derper
COPY build_cert.sh /app/build_cert.sh

CMD bash /app/build_cert.sh $DERP_HOST $DERP_CERTS /app/san.conf && \
/app/derper \
--hostname=$DERP_HOST \
--certmode=manual \
--certdir=$DERP_CERTS \
--stun=$DERP_STUN \
--a=$DERP_ADDR \
--http-port=$DERP_HTTP_PORT \
--verify-clients=$DERP_VERIFY_CLIENTS

build_cert.sh 脚本创建自签名证书

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
#!/bin/bash

CERT_HOST=$1
CERT_DIR=$2
CONF_FILE=$3

echo "[req]
default_bits = 2048
distinguished_name = req_distinguished_name
req_extensions = req_ext
x509_extensions = v3_req
prompt = no

[req_distinguished_name]
countryName = XX
stateOrProvinceName = N/A
localityName = N/A
organizationName = Self-signed certificate
commonName = $CERT_HOST: Self-signed certificate

[req_ext]
subjectAltName = @alt_names

[v3_req]
subjectAltName = @alt_names

[alt_names]
IP.1 = $CERT_HOST
" > "$CONF_FILE"

mkdir -p "$CERT_DIR"
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout "$CERT_DIR/$CERT_HOST.key" -out "$CERT_DIR/$CERT_HOST.crt" -config "$CONF_FILE"

构建镜像:

1
docker build -t zerchin/derper-ip:v0.1 .

总结

通过开源 Headscale 替代 Tailscale 控制平面,打破设备规模束缚并实现策略自主。自建 DERP 节点,将中继延迟从跨国绕行的几百 ms 压缩至 10ms 级。总而言之,自建 Headscale + DERP 真香!

参考链接:

https://warnerchen.github.io/2025/04/26/Tailscale-%E8%87%AA%E5%BB%BA%E4%B8%AD%E7%BB%A7/

https://icloudnative.io/posts/custom-derp-servers/
https://github.com/yangchuansheng/ip_derper/tree/main