使用 Headscale(并启用自带 DERP)部署私有 Tailscale 服务
先说方案
使用 Docker compose 编排 Headscale
+ Headplane
(一个很像 Tailscale 官方的 WebUI),使用 Caddy
来申请 TLS 证书(用 NGINX 也行,只是我懒,Caddy 可以帮我搞定 TLS 证书的自动申请和续期)并反向代理到 Docker 容器。
我直接用了 Headscale 自带的 DERP 服务,比网上找到的一些需要额外搭的 DERP 简单不少😎
需要准备的是一个域名和一台装好了 Docker 的服务器(如果需要买服务器,可以点后面链接购买腾讯云轻量应用服务器)。
后文会给出 Caddyfile
、compose.yml
、Headscale 和 Headplane 的配置文件示例,只需要简单修改(域名)即可直接使用。
啰嗦几句
其实早在几个月前就把 Headscale 搭起来了,但是自建 DERP 这玩意的文档比较少(因为老外直接用 Tailscale 提供的 DERP 节点就足够了,真正需要自建 DERP 的基本都是中国大陆用户,应该是个比较小众的群体了),当时一直没有把 Headscale 自带的 DERP 给配好,所以之前是只有 Headscale 本身,DERP 还是走的 Tailscale 官方的节点。
直到最近宽带套餐即将到期,问了电信客服,目前有的公网 IPv4 大概率会被回收,于是开始研究失去公网 IP 之后的解决方案:
- 使用公网 IPv6
- 使用(自建 DERP 的) Tailscale
恰好这几天回老家的时候折腾了一下 IPv6,发现还是挺麻烦的:首先是 RouterOS 自带的 DDNS 会把出口公网 IPv4(但其实拨号只拿到了内网地址)也添加上去,这样导致域名解析会拿到一个无法访问的 A 记录;其次是 IPv6 没法用 L2TP/IPSec (用 RouterOS 开启 L2TP 还是挺简单的)来搭回家的 VPN 了,而 IKEv2 的证书比较麻烦。
然后发现 Tailscale 其实是可以全局流量加密的(使用公共 Wi-Fi 时套一层 VPN 加密全部流量是我目前用公网 IPv4 最多的场景),于是又折腾了一下 Headscale,并且把自建 DERP 也弄好了,至此从广州电信连回老家的联通就不会绕道走 Tailscale 的洛杉矶节点 DERP 了
准备工作
目录结构
先创建目录,我是把所有配置都放在 /app/headscale
这个目录下了,目录结构如下:
.
├── compose.yml
├── config
│ └── config.yaml
├── headplane
│ └── config.yaml
├── lib
└── run
如果你参照我的目录结构,可以用下面命令创建目录
sudo mkdir -p /app/headscale/{config,headplane,lib,run}
Docker compose
这里用到的版本是 headscale/headscale:0.26.0
(发文时 0.26.1 已经发布了,你也可以直接用新版) 和 ghcr.io/tale/headplane:0.6.0
基于官方给的示例 compose.yaml
修改后保存到 /app/headscale/compose.yml
:
services:
headscale:
image: headscale/headscale:0.26.0
restart: unless-stopped
container_name: headscale
ports:
- "0.0.0.0:8080:8080" # headscale HTTP 用到的端口
- "0.0.0.0:9090:9090" # TODO: 这个端口好像不用也行,等我改天抽空确认一下
- "0.0.0.0:3478:3478" # DERP 用到的端口
volumes:
- /app/headscale/config:/etc/headscale
- /app/headscale/lib:/var/lib/headscale
- /app/headscale/run:/var/run/headscale
command: serve
labels:
# Headplane 需要用到的标签
me.tale.headplane.target: headscale
headplane:
image: ghcr.io/tale/headplane:0.6.0
container_name: headplane
restart: unless-stopped
ports:
- '3000:3000'
volumes:
- '/app/headscale/headplane/config.yaml:/etc/headplane/config.yaml'
# Headscale 的配置文件也要挂载进去,并且路径保持一致
- '/app/headscale/config/config.yaml:/etc/headscale/config.yaml'
# Headplane 的数据文件存放的目录
- '/app/headscale/lib:/var/lib/headplane'
# 只读挂载 Docker socket
- '/var/run/docker.sock:/var/run/docker.sock:ro'
Headscale 配置
先把官方的配置文件(https://github.com/juanfont/headscale/blob/main/config-example.yaml)复制下来放到 /app/headscale/config/config.yaml
,然后找到并修改以下部分:
# 服务器地址,这里改为你的域名,协议也改成 https(Caddy 会处理)
server_url: https://你的域名
# 监听地址,要从 127.0.0.1 改成 0.0.0.0
listen_addr: 0.0.0.0:8080
# 然后是 derp 这一部分:
derp:
server:
# 改为 true 启用 headscale 自带的 derp
enabled: true
# region_id 可以保持默认的 999
region_id: 999
# region_code 和 region_name 可以随意取,建议取个个性点的名字,方便后续确认是否正确使用自建的 DERP
region_code: "headscale-gz"
region_name: "Guangzhou"
# 确保这一项为 true,这样会自动推送给节点
automatically_add_embedded_derp_region: true
# ipv4 改成你服务器的 IP 地址,如果有 ipv6 的话也填上
ipv4:
ipv6:
# 到此为止是 derp.server 部分要修改的内容
# 接下来是 derp.urls 部分
url:
# 示例配置给了一份 tailscale 官方的 derpmap,如果你不想用 tailscale 的 derp 的话,就删掉或者注释掉
# - https://controlplane.tailscale.com/derpmap/default
Headplane 配置
也是先复制官方示例配置 https://github.com/tale/headplane/blob/main/config.example.yaml 到 /app/headscale/headplane/config.yaml
,然后修改以下部分:
server:
# 加密 cookie 用的,随机生成 32 个字符即可
cookie_secret:
# 安全 cookies,如果是 true 的话,那么 Headplane 就必须是 HTTPS 协议访问才行,因为我不打算把 Headplane 暴露在公网,后续要用也是用 Tailscale 连上服务器(因为这台服务器也会成为一个内网设备)之后通过内网 IP + http 访问,所以改为 false
cookie_secure: false
headscale:
# Headscale 实例的 URL,因为是用 Docker compose 编排的,所以可以直接用容器名 `headscale` 访问,端口也是刚刚配置的 8080
url: "http://headscale:8080"
启动!
配置文件已经准备好了,接下来把工作目录切换到 /app/headscale
,然后执行
docker compose up -d
过几秒执行 docker compose logs
看看输出,如无意外是没有报错的,那就说明启动成功了!
配置防火墙和 Caddy
需要在你的安全组里放通 TCP 443
和 3478
两个端口。
(如果没有安装 Caddy,可以参考官方文档 https://caddyserver.com/docs/install 用包管理器安装)
接下来配置 Caddy 让外部可以访问到 Headscale 服务。先把你的域名解析到你的服务器,然后在 /etc/caddy/Caddyfile
(一般来说是这个文件,如果不是的话,执行 ps ax | grep caddy
看看 --config
后面的文件哪个)增加一段配置:
<你的域名> {
reverse_proxy :8080
}
再执行 sudo service caddy reload
重载 caddy
,并且通过 sudo service caddy status
确认有无报错。
大功告成
如果前面的步骤都没有报错,那部署就完成了,在 Linux 上加入这个服务器就使用这个命令(要先安装 Tailscale,安装说明:https://tailscale.com/kb/1031/install-linux):
tailscale up --login-server https://<你的域名> # 后面可以按需加一些参数
而在图形客户端(iOS/macOS/Android/Windows 等等)里添加账号用 Add Account Using Alternate Server
,地址就填 https://<你的域名>
这两种方式都会弹出一个 Machine Key
,把这个 key 复制到 Headplane
里去 Add Device
-> Register Machine Key
。
哦对了,添加第一台设备之前,你是没法通过内网 IP 访问到 Headplane 的,这时候通过 SSH 端口转发一下,先打开一个新的终端模拟器窗口,执行:
ssh -L 3000:127.0.0.1:3000 <你的服务器IP或域名> -N
# 这里简单解释一下 3000:127.0.0.1:3000
# 第一个 3000 是指本机的端口;中间的 127.0.0.1 是指 SSH 连上服务器之后要转发的地址,即转发服务器本身;第二个 3000 是要转发的端口
# 合起来就是:把你访问本机 3000 (第一个 3000)端口的连接转发给你的服务器,让你的服务器去访问 127.0.0.1(即服务器自身) 的 3000 端口(第二个 3000)
然后用浏览器访问 http://127.0.0.1:3000/admin
即可打开 Headplane 了;第一次打开 Headplane 会让你输 API Key,并且它贴心地告诉你通过在服务器上执行 headscale apikeys create
即可得到 key,但是我们是 docker 运行的,命令就要稍加变化:
docker exec -it headscale headscale apikeys create
这里做个简单解释,方便你之后想执行其他 headscale 命令时参考:
docker exec -it
是在容器内交互式执行命令,后面紧跟着的第一个 headscale
是容器名,再之后的 headscale apikeys create
是要执行的命令,所以你也可以这样列出已经创建的 API Key:
docker exec -it headscale headscale apikeys list
扯远了,回到前面的端口转发,在你配置好机器之后,做端口转发的这个窗口直接 Ctrl + C
结束掉即可。如果你已经把你跑 Headscale 的这台服务器也添加到你的 Machines
里了,那么以后不需要再做端口转发了,可以连上 Tailscale 之后直接访问:http://<服务器的主机名或者内网 IP,一般是 100.64.x.x>:3000/admin
哦对了,确认下自建 DERP,当你添加了两台不在一个内网的机器,并且互相访问了,那么你在其中一台机器上可以:
- 如果是 Android 设备,那么点击你刚刚访问的那台设备,进去之后右上角有个仪表盘图标,点击就会进行 PING,这个时候可以看到要么是
Direct connection
(直连),要么是Relayed connection(HEADSCALE-GZ)
之类的,如果括号里就是前面你命名的 DERP region_code,那说明连上了自建 DERP 服务器 - 如果是 Linux,执行
tailscale status
、如果是 macOS,执行/Applications/Tailscale.app/Contents/MacOS/Tailscale status
,如果看到direct
则是直连,如果看到relay
并且后面有你自定义的 DERP 名字,那说明连上了自建 DERP - iOS 貌似看不到连接情况;Windows 我没有测试过,自行摸索一下