为什么选用tailscale?
笔者有异地组网需求,目前尝试过frp stcp做穿透、SoftEther组网,经过一段时间的使用,稍微总结一下:使用frp用stcp发布服务,客户端连接,确实是个很好的办法,但是配置终究太麻烦,需要安装客户端、在添加了穿透之后每个客户端需要独立配置等等;SoftEther 可能是当下最好的组网技术,低占用、linux内核级支持、配置简单,这些都是它的优点,可是有一个十分致命的点,它是中心化的,流量都需要过一遍server进行转发,这无异于极大的增加了服务器的带宽压力且增加了流量的延迟。经过这一段时间的查阅资料,根据朋友的安利,发现了tailscale这个基于SoftEther 的项目,他的工作原理可以通过官网进行查看,与此同时又发现了headscale这个对tailscale server的开源实现。
这里可能有人就要问了,为什么在tailscale官方免费提供了3用户100台设备连接、提供acl、p2p、sso的情况下,要选择自建headscale呢,当然是为了固定IP,毕竟随机分配的地址可太难受了。
如果您觉得tailscale官方提供的设备数量足够你使用,那么你仍然可以使用官方提供的服务,但仍建议您自建一个derp,因为官方未在大陆地区提供derp服务,存在人数较多及访问延迟较高的情况。
server端配置
如果你参照此配置部署headscale,只需要把headscale.example.com 和hsderp.example.com修改为自己的域名即可。
[root@iZwz9d5g0pgakn5rzwziz0Z headscale]# tree
.
├── docker-compose.yml
├── headscale
│?? ├── config
│?? │?? ├── config.yaml
│?? │?? ├── derp.yam
docker-compose.yml
services:
server:
image: registry.cn-hangzhou.aliyuncs.com/rhub/headscale:v0.27
# image: headscale/headscale::v0.27
container_name: headscale-server
volumes:
- ./headscale/config:/etc/headscale
- ./headscale/data:/var/lib/headscale
- ./headscale/run:/var/run/headscale
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
ports:
- "8080:8080"
command: serve
restart: unless-stopped
depends_on:
- derp
webui:
image: registry.cn-hangzhou.aliyuncs.com/rhub/headscale-ui:2025.08.23
# image: ghcr.io/gurucomputing/headscale-ui
container_name: headscale-ui
environment:
HTTP_PORT: 7070
ports:
- "7070:7070"
volumes:
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
restart: unless-stopped
derp:
image: registry.cn-hangzhou.aliyuncs.com/rhub/fredliang.derper:v1.90.6
# image: fredliang/derper:v1.90.6
container_name: headscale-derp
environment:
DERP_DOMAIN: hsderp.example.com
DERP_ADDR: :6060
DERP_CERT_MODE: letsencrypt
DERP_VERIFY_CLIENTS: true
ports:
- "6060:6060" # derp port, TCP
- "3478:3478/udp" # STUN port, UDP
volumes:
- ./var/run/tailscale:/var/run/tailscale
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
restart: unless-stopped
client:
image: registry.cn-hangzhou.aliyuncs.com/rhub/tailscale:stable
# image: tailscale/tailscale:stable
container_name: tailscale-client
hostname: nhub-aliyun-derp
privileged: true
environment:
TS_EXTRA_ARGS: --netfilter-mode = off
volumes:
- ./var/run/tailscale:/var/run/tailscale
- /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
depends_on:
- derp
config.yaml
server_url: https://headscale.example.com
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 0.0.0.0:9090
grpc_listen_addr: 0.0.0.0:50443
noise:
private_key_path: ./noise_private.key
prefixes:
v6: fd7a:115c:a1e0::/48
v4: 100.100.0.0/16
allocation: sequential
derp:
paths:
- /etc/headscale/derp.yaml
database:
type: sqlite
sqlite:
path: /var/lib/headscale/db.sqlite
write_ahead_log: true
dns:
override_local_dns: false
magic_dns: false
[root@iZwz9d5g0pgakn5rzwziz0Z headscale]# cat headscale/config/
derp.yaml
regions:
901:
regionid: 901
regioncode: nhubsz
regionname: nhub aliyun Cloud
nodes:
- name: my-derp
regionid: 901
hostname: hsderp.example.com
stunport: 3478
stunonly: false
derpport: 443
902:
regionid: 902
regioncode: xxx
regionname: xxx aliyun Cloud
nodes:
- name: xxx-derp
regionid: 902
hostname: derp1.xxx.com
stunport: 3488
stunonly: false
derpport: 6060nginx反向代理配置
[root@iZwz9d5g0pgakn5rzwziz0Z conf.d]# cat headscale.example.com.conf
upstream headscale-server {
server 127.0.0.1:8080 max_fails=2 fail_timeout=10s;
}
upstream hs-web {
server 127.0.0.1:7070 max_fails=2 fail_timeout=10s;
}
server {
listen 0.0.0.0:80;
listen 443 ssl;
http2 on;
server_name headscale.example.com;
if ($scheme = http ) {
return 301 https://$host$request_uri;
}
ssl_certificate cert.d/example.com.pem;
ssl_certificate_key cert.d/example.com.key;
ssl_session_timeout 5m;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!3DES:!ADH:!RC4:!DH:!DHE;
ssl_prefer_server_ciphers on;
include proxy.conf;
location / {
proxy_pass http://headscale-server;
}
location ^~ /web {
proxy_pass http://hs-web;
add_header Cache-Control no-cache;
}
# 配置错误页面
include error.conf;
}[root@iZwz9d5g0pgakn5rzwziz0Z conf.d]# cat hsderp.example.com.conf
upstream hsderp-server {
server 127.0.0.1:6060 max_fails=2 fail_timeout=10s;
}
server {
listen 0.0.0.0:80;
listen 443 ssl;
http2 on;
server_name hsderp.example.com;
if ($scheme = http ) {
return 301 https://$host$request_uri;
}
ssl_certificate cert.d/example.com.pem;
ssl_certificate_key cert.d/example.com.key;
ssl_session_timeout 5m;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!3DES:!ADH:!RC4:!DH:!DHE;
ssl_prefer_server_ciphers on;
include proxy.conf;
location / {
proxy_pass http://hsderp-server;
}
# 配置错误页面
include error.conf;
}[root@iZwz9d5g0pgakn5rzwziz0Z conf.d]# cat ../proxy.conf
proxy_redirect off;
client_body_timeout 10s;
client_header_timeout 10s;
client_max_body_size 10m;
client_body_buffer_size 128k;
proxy_connect_timeout 60;
proxy_send_timeout 60;
proxy_read_timeout 60;
proxy_buffers 32 32k;
proxy_buffer_size 64k;
proxy_set_header Accept-Encoding " ";
proxy_intercept_errors on;
proxy_headers_hash_max_size 102400;
proxy_headers_hash_bucket_size 12800;
proxy_hide_header X-Powered-By;
proxy_hide_header X-AspNet-Version;
proxy_busy_buffers_size 128k;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
# 传递原始的HTTPS相关头
proxy_set_header X-Forwarded-Ssl on;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Frame-Options SAMEORIGIN;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
# 添加header
add_header X-Cache $upstream_cache_status;
add_header Strict-Transport-Security "max-age=31536000";
add_header Cache-Control no-cache;
# 指定字符集,防止中文乱码
charset utf-8;
# 使用gzip压缩
gzip on;
gzip_static on;
gzip_proxied any;
gzip_http_version 1.0;
gzip_min_length 1100;
gzip_buffers 4 16k;
gzip_comp_level 6;
gzip_vary on;
gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/json application/javascript image/x-icon;
gzip_disable "MSIE [1-6]\.";headscale-ui配置
为了便捷控制,首先生成api key供headscale-ui进行控制 ,其中-e参数后面指定的是该apikey的过期时间,这里设置为720天
docker exec headscale-server headscale apikeys create -e 3600dui端配置
2.1 打开ui端页面
2.2 点击”Settings”
2.3 添加”Headscale URL”,本例为 https://headscale.example.com
2.4 将刚才生成的api key粘贴进“Headscale API Key”中
2.5 点击“Test Server Settings”,出现绿色对号后UI端就可以通过ui控制服务端了
2.6 进入“User View”,点击“+New User”,添加一个用户
2.7 为该用户生成一个Preauth Key,供客户端连接使用。为了便捷性,最好设置为“Reusable”,并“Active”
client
linux
docker部署
我搭建的过程linux只要用来推送子路由,所以宿主机本身没有连入VPN。(宿主机如果需要使用VPN网络,取消配置"# - /dev/net/tun:/dev/net/tun"和"network_mode: "host"的注释即可)
services:
client:
image: registry.cn-hangzhou.aliyuncs.com/rhub/tailscale:stable
# image: tailscale/tailscale:stable
hostname: tailscale-client
privileged: true
# network_mode: "host"
environment:
TS_EXTRA_ARGS: --netfilter-mode = off
volumes:
- ./tailscale:/var/run/tailscale
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
- ./lib:/var/lib
# - /dev/net/tun:/dev/net/tun
cap_add:
- net_admin
- sys_module
command: tailscaled
restart: unless-stopped客户端登入headscale服务器
docker exec tailscale-client tailscale up --accept-routes=true \
--advertise-routes=192.168.10.0/24 \
--login-server=https://headscale.example.com \
--auth-key=xxxxxxd4e41904ed95290a107e5ab56a94f2axxxxxxxxxxx--accept-routes=true: 接受子网路由(当对端广告子网时)。
--advertise-routes=192.168.10.0/24: 本机作为子网路由器。
--netfilter-mode=off: 容器/受控环境里不希望触碰防火墙,或者你将自行管理路由与规则。
--reset: 重置本机的 Tailscale 偏好配置为默认值,不退出 tailnet、也不删除节点。
windows
Windows 默认不允许 Tailscale 作为路由器,需要额外修改的配置过多,所以不推荐使用windows发布子路由。
tailscale up \
--login-server=https://headscale.example.com \
--auth-key=xxxxxxd4e41904ed95290a107e5ab56a94f2axxxxxxxxxxx常用命令
ip route show table 52 :查看 Linux 内核中由 Tailscale 管理的策略路由表(编号 52)里的路由项。
tailscale status:查看当前设备与所有已连接对端的状态。
tailscale netcheck:进行网络连通性自检,包括 NAT 类型、UDP 打洞能力、IPv6 可用性、到各 DERP 区的延迟与选择。
tailscale netcheck --verbose:更详细的自检信息。
tailscale debug derp mirayaisz: 测试与derp服务器mirayaisz的连通性。
tailscale down:断开并清理本机配置。
tailscale ip -4/-6 :显示本机 Tailscale IP。
derp
docker部署
services:
derp:
image: registry.cn-hangzhou.aliyuncs.com/rhub/fredliang.derper:v1.90.6
# image: fredliang/derper:v1.90.6
environment:
DERP_DOMAIN: derp1.rd.chn-das.com
DERP_ADDR: :6060
DERP_STUN_PORT: 3478
DERP_CERT_MODE: manual
ports:
- "6060:6060"
- "3478:3478/udp"
volumes:
- /opt/derp/certs:/app/certs
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
restart: alwaysderp上面的客户端只是为了给derp服务做验证校验,所以不需要接受子网路由。
tailscale up --netfilter-mode=off --accept-routes=false --login-server=https://headscale.example.com --auth-key=xxx423499652927fc7457f13d86921a7bbd5a9xxx防止 DERP 被白嫖
services:
derp:
image: registry.cn-hangzhou.aliyuncs.com/rhub/fredliang.derper:v1.90.6
# image: fredliang/derper:v1.90.6
environment:
DERP_DOMAIN: derp1.rd.chn-das.com
DERP_ADDR: :6060
DERP_STUN_PORT: 3478
DERP_CERT_MODE: manual
DERP_VERIFY_CLIENTS: true
ports:
- "6060:6060"
- "3478:3478/udp"
volumes:
- /opt/derp/certs:/app/certs
- ./run/tailscale:/var/run/tailscale
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
restart: always
client:
image: registry.cn-hangzhou.aliyuncs.com/rhub/tailscale:stable
# image: tailscale/tailscale:stable
hostname: das-aliyun-derp
privileged: true
environment:
TS_EXTRA_ARGS: --netfilter-mode = off
volumes:
- ./run/tailscale:/var/run/tailscale
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
- ./var/lib:/var/lib
cap_add:
- net_admin
- sys_module
command: tailscaled
restart: always
depends_on:
- derp- ./run/tailscale:/var/run/tailscale将tailscale.sock套接字接口挂载进容器中。DERP_VERIFY_CLIENTS: trueDERP 会验证连接的客户端是否与本机的客户端为同一个hs服务下,从而避免其他客户端白嫖服务器。
踩坑记录
Q:同一个局域网内两个客户端节点,其中一台A发布了子路由,另外一台B接受子路由,会导致B路由冲突,导致整个内网默认路由都走A。
A:同一个局域网内有多个节点的时候,要么都不发布子路由,要么除发布子路由的节点其他节点使用--accept-routes=false不接受路由推送。
引用文档
引用的文档普遍有点小问题,仅供参考,以本文配置为准。