tailscale 自建服务器
为什么是VPN?
前面讲过,我企图打通各个住所和学校的内网。列位要问了,你不是搞过FRP内网穿透吗,为啥还要VPN?我个人的理解是:FRP侧重于服务,依托于开放的端口;VPN侧重于互连,依托于C/S架构和IP,对比于下表。可见,要打通各个内网,必须使用基于VPN的技术才行。
| 对比项 | FRP | VPN |
|---|---|---|
| 开放端口数 | 随服务数增加 | 很少 |
| 主要应用 | 对外提供服务,网页服务较多 | 对内提供连通 |
| 穿透方向 | 单向 | 双向(通过路由) |
| 安全性 | 一般 | 强 |
| IP级互连 | 不支持 | 支持 |
| 额外的客户端 | 不需要 | 一般需要 |
| 部署难度 | 容易 | 困难 |
用哪个VPN?
关于主流VPN技术,下面这篇文章总结的挺好。
我斗胆再一句话总结下:PPTP不安全;OpenVPN针对IPSec/L2TP做了减法;WireGuard针对OpenVPN又做了减法,性能更高,还支持了去中心化。
可见,WireGuard是目前最先进的VPN技术,已被引入Linux内核,必须选她!
还有个原因,群晖的VPN服务端都被阉割了,自己装套件起
OpenVPN也不行;威联通的OpenVPN服务端可以,但静态路由设置时总是出错。
为什么是Headscale?
WireGuard目前只是一个内核级别的模块,想要配置好裸的WireGuard,低代码是别想了,那么多对端秘钥,增、删节点都需要改动所有节点的配置,想一想就头疼!
表扬威联通,已经支持图形化界面的
WireGuard服务器和客户端。
基于WireGuard的上层应用,目前比较成熟的有Tailscale和Netmaker。Tailscale 是在用户态实现了 WireGuard 协议;Netmaker 直接使用了内核态的WireGuard,理论上性能更高,但目前缺乏中继机制(类似FRP),应用场景受限。Headscale是Tailscale的开源实现,适合私有部署,就选她了!
本文动机
知乎上介绍Headscale的很少;找遍全网,也很少有低代码、快速部署Headscale的文章,能讲清楚原理和为什么这样配置的就更少了。
仍然要感谢一些博主,虽然不讲原理,但内容确实丰富,给我一定启发(其实是偷懒不用去看文档了),比如下面这个。
Tailscale玩法之内网穿透、异地组网、全隧道模式、纯IP的双栈DERP搭建、Headscale协调服务器搭建,用一期搞定,看一看不亏吧?
我在群晖和威联通的NAS上都用docker-compose部署成功了,必须向大家汇报下,希望能帮助更多非专业领域的“私有云折腾师”。
Headscale搭建
架构介绍
主节点(我自己定义的概念)的网络拓扑如下图所示。其他节点与之类似,不包含服务端及其UI。

主节点网络拓扑
服务端(server),又叫协调服务器。负责WireGuard节点的公钥交换、虚拟IP分配、路由转发的公开和访问控制。
客户端(client),即WireGuard节点。目前仍然使用的是Tailscale的开源客户端,采用go语言编写,在用户空间实现WireGuard。
中继端(derp),是P2P连接时NAT穿透的保底方案。DERP(Detoured Encrypted Routing Protocol)是Tailscale自研的协议,运行在 HTTP 之上 ,根据目的公钥来中继加密的流量。中继端同时支持DERP和STUN。
关于NAT穿透的原理,可以参考下面这篇。
NAT穿透arthurchiao.art/blog/how-nat-traversal-works-zh/
可见,服务端负责控制,中继端负责数据通路,客户端发起/接受连接,是可以部署在不同的服务器上的。这里我们资源有限,把他们都部署在一个NAS里,还需要使用反向代理(lucky)以“零代码”支持带SSL证书的HTTPS访问;为了“低代码”配置服务端,我们给她再加一个服务端控制界面(webui,以下简称UI端),齐活。
关于客户端,其实有两个作用。一是做为WireGuard的节点连到大内网里。
这时,为了减少路由的层级,其容器的网络类型一般设为host。
二是通过Unix的进程间通信(sock)为中继端提供用户认证,防止中继端被他人使用。
通过把客户端和中继端的/var/run/headscale链接在一起来实现。这时,其容器的网络类型最好设为bridge。
如何选择容器网络类型,可以参考下面的公式。
假设,中继端部署在服务器A上,负责VPN路由的是服务器B。
if(A == B)
在A上部署客户端,容器网络使用host。
else {
在A上部署客户端,容器网络使用bridge或host都行。
在B上部署客户端;如果使用容器,其网络使用host。// 例如,OpenWRT上可以直接部署。
}
关于自定义的容器子网,可以参考下面这篇文章。
我把这些容器都部署在一个NAS上,所以用host。相关的端口如下表,使用了基于子域名的lucky反向代理后,只需要对公网(别忘了在路由器上做端口映射)暴露一个STUN的UDP3478端口(新增)和一个lucky反向代理的端口(例如8080,已有)。相比FRP,美极了。
| 服务端 | UI端 | 中继端DERP | 中继端STUN | |
|---|---|---|---|---|
| 端口类型 | TCP | TCP | TCP | UDP |
| 容器侧端口 | 8080 | 7070 | 6060 | 3478 |
| NAS侧端口 | 58080 | 57070 | 56060 | 3478 |
| HTTPS反向代理 | 需要 | 需要 | 需要 | 不需要 |
容器编排
直接给出带注释的四合一docker-compose.yaml,全网罕见。
version: '3.9'
networks: # 定义编排容器的子网
private:
driver: bridge
ipam:
config:
- subnet: 172.28.200.0/24
services:
server: # 服务端
image: headscale/headscale
container_name: headscale-server
networks:
- private
volumes:
- ./headscale/config:/etc/headscale # 提前放好config.yaml和derp.yaml
- ./headscale/data:/var/lib/headscale
- ./headscale/run:/var/run/headscale
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro # 使用NAS的时间
ports:
- "58080:8080" # listen port
command: serve # v0.22及以前的版本需要使用headscale serve
restart: unless-stopped
depends_on:
- derp
webui: # UI端
image: ghcr.io/gurucomputing/headscale-ui
container_name: headscale-ui
networks:
- private
environment:
HTTP_PORT: 7070
ports:
- "57070:7070"
volumes:
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
restart: unless-stopped
derp: # 中继端
image: fredliang/derper
container_name: headscale-derp
networks:
- private
environment:
DERP_DOMAIN: derp.bakeding.site # 替换为自己的域名
DERP_ADDR: :6060 # 注意,前面有个英文冒号
DERP_CERT_MODE: letsencrypt # 使用了lucky做反向代理,理论上不需要设置,但我还没试过。
DERP_VERIFY_CLIENTS: true # 还用client做认证时,配置为true
ports:
- "56060:6060" # derp port, TCP
- "3478:3478/udp" # STUN port, UDP
volumes:
- ./tailscale:/var/run/tailscale
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
restart: unless-stopped
depends_on:
- client
client: # 客户端
image: tailscale/tailscale
container_name: headscale-client
network_mode: "host" # 用做连接各子网的客户端时,这样最简单
privileged: true
environment:
TS_EXTRA_ARGS: --netfilter-mode = off # 默认不开启路由转发,更灵活
volumes:
- ./tailscale:/var/run/tailscale # 要在NAS上和derp共享同一个目录
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
- /var/lib:/var/lib
- /dev/net/tun:/dev/net/tun
cap_add:
- net_admin
- sys_module
command: tailscaled
restart: unless-stopped
注意,要提前配置好config.yaml和derp.yaml。可以去GitHUB的代码仓,下载config-example.yaml和derp-example.yaml,修改好内容(见下文)并重命名。
我用的是
latest映像,当前对应源码的版本是v0.23.0-alpha5。配置文件如果报错,可以去搜一下Issues,一般都有答案。
另外,只需要把docker-compose.yaml中server和webui的部分注释掉,就可以部署在其他节点。如果不想增加中继端,也可以把derp的部分注释掉。
服务端配置
config.yaml中修改的地方如下。
server_url要改成反向代理后的网址。- 把
urls下面的网址注释掉,不使用官方的中继端。 - 增加
derp.yaml的位置,指定自己搭建的中继端。 - 注意各端口要和
docker-compose.yaml中的对应。
server_url: https://tailscale.bakeding.site
listen_addr: 0.0.0.0:8080
# Address to listen to /metrics, you may want to keep this endpoint private to your internal network
metrics_listen_addr: 0.0.0.0:9090
grpc_listen_addr: 0.0.0.0:50443 # 看起来没啥用
ip_prefixes:
100.100.0.0/16
# List of externally available DERP maps encoded in JSON
urls:
#- https://controlplane.tailscale.com/derpmap/default
# Locally available DERP map files encoded in YAML
paths:
- /etc/headscale/derp.yaml
derp.yaml如下,这里我添加了两个中继端。
# If you plan to somehow use headscale, please deploy your own DERP infra: https://tailscale.com/kb/1118/custom-derp-servers/
regions:
901:
regionid: 901
regioncode: huoyan
regionname: tencent
nodes:
- name: tencent-derp
regionid: 901
hostname: derp.bakeding.site
stunport: 3478
stunonly: false
derpport: 56060
902:
regionid: 902
regioncode: hk
regionname: Hongkong Telecom
nodes:
- name: shelter2-derp
regionid: 902
hostname: derp.mirror.example.com
stunport: 3478
stunonly: false
derpport: 56060
反向代理配置
headscale-server和derp正常进行反代就行了,ui端因为跨域的原因,需要在同一个域里。如您按照本文提供的内容进行创建的话,那么你可以直接使用此nginx配置,否则请自行修改相应端口。
server {
#监听443端口
listen 443 ssl;
#你的域名
server_name tailscale.bakeding.site;
access_log /var/log/nginx/tailscale443.log;
#ssl证书的pem文件路径
ssl_certificate /www/cert/tailscale.bakeding.site_bundle.pem;
#ssl证书的key文件路径
ssl_certificate_key /www/cert/tailscale.bakeding.site.key;
location ^~ / {
proxy_pass http://127.0.0.1:58080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
add_header X-Cache $upstream_cache_status;
add_header Strict-Transport-Security "max-age=31536000";
add_header Cache-Control no-cache;
}
location ^~ /web {
proxy_pass http://127.0.0.1:57070;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
add_header X-Cache $upstream_cache_status;
add_header Strict-Transport-Security "max-age=31536000";
add_header Cache-Control no-cache;
}
}
server {
listen 80; #监听80端口
listen [::]:80;
server_name tailscale.bakeding.site; #也可以填写自己注册的域名
access_log /var/log/nginx/default.log;
location ^~ / {
proxy_pass http://127.0.0.1:56060;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
add_header X-Cache $upstream_cache_status;
add_header Strict-Transport-Security "max-age=31536000";
}
location ^~ /web {
proxy_pass http://127.0.0.1:57070;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
add_header X-Cache $upstream_cache_status;
add_header Strict-Transport-Security "max-age=31536000";
add_header Cache-Control no-cache;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html; #错误页面设置
location = /50x.html {
root /usr/share/nginx/html;
}
}
server {
#监听443端口
listen 443 ssl;
#你的域名
server_name derp.bakeding.site;
access_log /var/log/nginx/derp443.log;
#ssl证书的pem文件路径
ssl_certificate /www/cert/derp.bakeding.site_bundle.pem;
#ssl证书的key文件路径
ssl_certificate_key /www/cert/derp.bakeding.site.key;
location ^~ / {
proxy_pass http://127.0.0.1:56060;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
add_header X-Cache $upstream_cache_status;
add_header Strict-Transport-Security "max-age=31536000";
}
}
server {
listen 80; #监听80端口
listen [::]:80;
server_name tailscale.bakeding.site; #也可以填写自己注册的域名
access_log /var/log/nginx/default.log;
location ^~ / {
proxy_pass http://127.0.0.1:56060;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
add_header X-Cache $upstream_cache_status;
add_header Strict-Transport-Security "max-age=31536000";
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html; #错误页面设置
location = /50x.html {
root /usr/share/nginx/html;
}
}
服务端操作(步骤1)
其实服务端支持很多命令行操作,但我们追求“低代码”,只需要用命令行生成一个API Key,剩下的工作在UI端点鼠标就行了。
进入容器,执行命令,把生成的API Key记录下来:ynREpBS.UduOZ_Dwxu1WRreh6vMKvUsiNwvefz_n
$ headscale apikeys create -e 9999d
其中,-e后面指定的是过期时间,这里我指定9999天,27年后看能否有人攻破。
也可以在宿主机上执行,前面加
sudo docker exec -it即可,不会的可以练练。
UI端操作(步骤2)
- 打开UI的URL,本例为
https://tailscale.bakeding.site/web。
2. 进入“Settings”。
3. 添加“Headscale URL”,本例为https://tailscale.bakeding.site。
4. 把服务端生成的Key添加到“Headscale API Key”。
5. 点击“Test Server Settings”,出现绿色对号后UI端就可以接管服务端了,如下图所示。

UI端添加API Key
6. 进入“User View”,点击“+New User”,添加一个用户。

UI端添加用户
7. 为该用户生成一个Preauth Key,供客户端连接使用。为了便捷性,最好设置为“Reusable”,并“Active”,如下图。

UI端添加Preauth Key
连接的密钥设置比较灵活,有两种方法。一种是上面这种:在服务端生成Preauth Key(1个共享或多个独立),客户端连接时指定,成功后在“Device View”里就能看到各个节点。另一种是在客户端连接时生成,在UI端的“Device View”里手动添加秘钥、注册节点。我这么懒惰,当然共享1个Preauth Key。
客户端操作(步骤3)
- 进入各客户端的容器,执行命令。
tailscale up --netfilter-mode=off \
--accept-routes \
--advertise-routes=192.168.2.0/24 \
--login-server=https://tailscale.bakeding.site \
--auth-key=c5765d7426fe55c005ffda74419ec38f9a32fd770fa13199
--accept-routes代表接受其他节点的路由指示。--advertise-routes指定本节点对其他节点的路由建议,即哪个网段走VPN到本节点。一般是本节点的内网网段。--login-server指定服务端的URL。--auth-key指定在UI端生成的Preauth Key。
2. 打开UI端网页,进入“Device View”,把各节点的“Device Routes”设置为“active”,如下图。
这里还可以看到各个节点分配的VPN IP地址。

UI端开启Devic Routes
NAS配置(步骤4)
要在NAS上开启路由转发,把VPN路由过来的包转发到内网。
-
通过ssh登录到NAS,执行命令。
$ ip addr
2. 找到NAS的内网IP地址所对应的虚拟网卡名,我这里是ovs_eth0;找到VPN地址所对应的网卡名,我这里是tailscale0。
3. 执行命令:启用IPv4转发功能;防火墙配置了两个网络接口(ovs_eth0和tailscale0)的数据包转发规则,并执行网络地址转换(NAT)操作。使能了VPN子网和内网的双向互访。
sudo iptables -I FORWARD -i ovs_eth0 -j ACCEPT
sudo iptables -I FORWARD -o ovs_eth0 -j ACCEPT
sudo iptables -t nat -I POSTROUTING -o ovs_eth0 -j MASQUERADE
sudo iptables -I FORWARD -i tailscale0 -j ACCEPT
sudo iptables -I FORWARD -o tailscale0 -j ACCEPT
sudo iptables -t nat -I POSTROUTING -o tailscale0 -j MASQUERADE
sudo sysctl -w net.ipv4.ip_forward=1
4. 最后,把它们加到群晖的“计划任务”,开机触发启动。
- 去掉所有
sudo,以root执行。 - 为了保证VPN相关的容器先启动,最上面最好加个
sleep 1m。
主路由配置(步骤5)
为了让本节点内网的其他地址也能通过VPN访问其他节点的内网,需要在主路由上添加静态路由,例如下表。
| 描述 | 目的地址 | 子网掩码 | 下一跳地址 | 出接口 |
|---|---|---|---|---|
| 访问VPN节点 | 100.100.0.0 | 255.255.0.0 | 本节点NAS地址 | LAN |
| 访问其他节点的内网 | 其他节点的内网网段 | 其他节点的内网掩码 | 本节点NAS地址 | LAN |
经过ping测试,大功告成!
参考文章 Tailscale/Headscale自建异地组网 Tailscale+Headscale+自建Derp踩坑记录 已验证:自建Tailscale的 DERP 中继教程
No comments to display
No comments to display