Headscale + Headplane 三端多活组网折腾实录(异地办公 + 多重安全)
从”家里想直连公司内网”这个朴素需求出发,一路做到 hk_main / fn-NAS / DSM 三端 Headscale 完全独立多活 + 自建 DERP + Headplane 管理面板 + Authelia 2FA 前置鉴权 + ACL autoApprovers + DB 单向同步的完整方案。
本文是笔记性质,尽量把过程中的坑、判断、命令、边界情况都记下来,方便日后翻阅。
一、背景与目标
在家和公司之间需要一条稳定、低延迟、可控的私网通道:
- 异地办公:家里的 Mac 要能像在公司局域网一样访问
gitlab.corp.example、10.20.30.40这类内网资源。 - 完全自主:不依赖 Tailscale 官方控制面,也不依赖 Tailscale 官方 DERP —— 数据面、控制面、管理面都在自己手里。
- 多重安全保障:控制面本身要有认证、管理面要有 2FA、tailnet 之间要有 ACL 授权、路由 auto-approve 要受 user 约束。
- 多活容灾:任何一台服务器(香港 VPS / 家里 NAS / 群晖)挂了,都能秒切到另一台,不影响使用。带宽差异大(香港带宽小、家里 IPv6 大带宽),要能自由选。
技术栈的选择:Headscale(Tailscale 控制面的开源实现,纯 Go,兼容官方 tailscale 客户端)+ Headplane(Headscale 的第三方 Web 管理面板,替代常见但已落后的 goodieshq/headscale-admin)+ Authelia(SSO + 2FA,保护 Web UI)+ s-ui / Clash(客户端出站分流,保证 Headscale/DERP 流量走直连)。
二、总体架构
三台机器各自跑一份 Headscale + Headplane,DB 由 hk_main 单向同步到 DSM 和 fn-NAS,客户端通过 switch_headscale.sh 秒切 profile。
| 项 | hk_main (香港 VPS) | fn-NAS (家里飞牛) | DSM-6400 (家里群晖) |
|---|---|---|---|
| 控制面 URL | https://headscale.example.com |
https://headscale.home.example.net:4433 |
https://headscale.nas.example.net:4433 |
| DERP region | 901 hkmain |
902 fnnas |
(共用 hk_main 中继) |
| DERP HTTPS 端口 | 443 (haproxy → nginx → headscale) | 4433 | — |
| STUN UDP 公网端口 | 3478 | 3578 (Lucky 转发到内网 3478) | — |
| 管理面板 | Headplane 0.7.1 /admin/ |
同左 | 同左 |
| Authelia 2FA | auth.example.com |
auth.home.example.net:4433 |
auth.nas.example.net:4433 |
| 依赖 Tailscale 官方 DERP | ❌ | ❌ | ❌ |
| DB 是否是主 | ✅ 主 | ⬅ 单向同步自 hk | ⬅ 单向同步自 hk |
关键设计约束:
- 三端 DB 完全一致(同一份 users / nodes / api_keys / routes / policy),只有
cookie_secret和证书这类”机器身份”独立。 - 三端只共享同一把 Headplane 登录 API key,人不用背三串密码。
- 三端各自发布只包含自己 DERP region 的 DERPMap,互不依赖 —— 挂一台不影响另外两台。
- 客户端 profile 之间共享
machine key,切服务端只是切”谁记账”,路由批准状态跟 DB 走。
三、多重安全体系
从外向内一共四层:
3.1 第一层:域名/IP 层的最小暴露面
- 控制面 URL 全部走 HTTPS + TLS,前面是 haproxy / nginx,后端 headscale 只监听 127.0.0.1 或容器内部。
4433这类高位端口 + IPv6 通道进一步降低被自动化扫描的概率。- STUN UDP
3478/3578是必须开的,但只承载加密的 STUN 探测和 DERP 数据面,本身不接受任何鉴权凭据。
3.2 第二层:Authelia 2FA 前置管理面
Headplane 管理面挂在 nginx 的 /admin/ 下面,nginx 在这个 location 上通过 auth_request 转到 Authelia。关键是只在 /admin/ 上加,其他 location 全部放行:
# 只有浏览器访问的 /admin/ 走 Authelia
location /admin/ {
include /etc/nginx/authelia-headscale.conf; # auth_request → Authelia
proxy_pass http://headplane:3000;
}
# 客户端所有 API 路径完全不受影响
location = /health { proxy_pass http://headscale:8080; }
location /api/v1/ { proxy_pass http://headscale:8080; } # Bearer token
location /ts2021 { proxy_pass http://headscale:8080; } # Noise
location /derp { proxy_pass http://headscale:8080; }
location /machine/ { proxy_pass http://headscale:8080; }
location /register/ { proxy_pass http://headscale:8080; } # tailscale up 授权
判断依据:tailscale 守护进程是机器对机器通信,不理解浏览器的 302,一旦被 Authelia 拦立刻断线。而管理面板是浏览器场景,上 2FA 天然合适。
踩过的坑:
/admin/返回403而不是期望的401 → 302,一开始怀疑规则写错。真正原因是configuration.yml更新后 Authelia 容器没重启,进程里的规则表还是旧版本,请求匹配不到任何规则,落到default_policy: deny就返回 403。以后凡是改authelia/config/configuration.yml都必须docker restart authelia。
3.3 第三层:Headscale 自身的 API Key + Bearer
Headscale CLI / Headplane / 自动化脚本都通过 API Key(形如
hskey-api-xxx.yyy...)访问/api/v1/*。API Key 有过期时间,可以随时
headscale apikeys expire撤销。生成方式:
docker exec headscale headscale apikeys create -e 87660h # 10 年注意:API Key 存在 SQLite DB 里,通过后面的 DB 同步机制自动分发到三端。Headplane 用的就是这把 key。
3.4 第四层:ACL + autoApprovers
Headscale 0.29 的策略用 HuJSON,示例:
{
"acls": [
{ "action": "accept", "src": ["*"], "dst": ["*:*"] }
],
"autoApprovers": {
"routes": {
"10.0.0.0/8": ["alice@"],
"172.16.0.0/12": ["alice@"],
"192.168.0.0/16": ["alice@"]
}
}
}
要点:
autoApprovers.routes的 owner 必须是 typed entity(0.29 新校验):alice@(user)/group:admin/tag:server之一。从 0.23 升级过来直接写"alice"会启动失败,报policy load failed。- 只对该 user 拥有的节点生效:以后加家人账号
family@就得追加,或者干脆改成group:admin更灵活。 - ACL 主体规则
action:accept src:* dst:*:*是”全放行”,家庭多活场景够用,如果要做部门级隔离再往里加。
四、Headplane 替换过时的 headscale-admin
历史包袱:项目起初用的是 goodieshq/headscale-admin(社区版),随着 Headscale 升级到 0.29 后 API Key prefix 显示行为变了,admin UI 直接白屏。有人尝试通过在容器里挂一份 fork 的 admin-web/ 静态资源做 JS shim 兼容,短期能跑,但:
admin-web/不在 git 里,谁改的、改了啥全靠猜;_app/immutable/目录被强行改名做浏览器缓存 bust,跟真实 JS 逻辑修改混一起看不清 diff;- 上游一发新版就散架,
docker compose pull直接覆盖挂载点。
正确解:换 ghcr.io/tale/headplane:0.7.1 —— 主动维护、官方声明支持 headscale 0.29,走标准的 docker-integration 模式(通过容器 label me.tale.headplane.target=headscale 找 headscale + docker.sock 触发命令)。
4.1 docker-compose.yml 片段
services:
headscale:
image: headscale/headscale:0.29.3
labels:
me.tale.headplane.target: headscale # ← Headplane 靠这个 label 定位
volumes:
- ./data:/var/lib/headscale
- ./config:/etc/headscale
ports:
- "127.0.0.1:8080:8080"
- "3478:3478/udp" # STUN
headplane:
image: ghcr.io/tale/headplane:0.7.1
depends_on: [headscale]
volumes:
- ./headplane/config.yaml:/etc/headplane/config.yaml:ro
- /var/run/docker.sock:/var/run/docker.sock # 让 headplane 能 reload headscale
ports:
- "127.0.0.1:3000:3000"
4.2 headplane 的 config.yaml
server:
host: "0.0.0.0"
port: 3000
cookie_secret: "<32 字节 base64,每台机器独立>"
cookie_secure: true
headscale:
url: "http://headscale:8080" # 容器内直连
config_path: "/etc/headscale/config.yaml" # headplane 会通过它读元数据
integration:
docker:
enabled: true
container_name: headscale
socket: "unix:///var/run/docker.sock"
oidc:
disable_api_key_login: false # 允许用 API Key 登录
登录时输入的就是前面生成的 hskey-api-xxx... 完整值。三端 DB 一致,同一把 key 三端通用。
4.3 secrets 落在 git 的取舍
headplane/config.yaml 明文含 cookie_secret + api_key。这个仓库里 db.sqlite、noise_private.key 也都是明文进 git 的,所以按同一惯例处理(仓库私有)。如果仓库有公开风险,一定要改用 cookie_secret_path + docker secrets / env 变量注入。
五、自建 DERP:从”依赖官方”到”三端完全独立”
5.1 为什么要自建
之前 tailscale 在家里 ping 公司 IP 延迟 800ms+,公司机 ping 家里资源也慢。tailscale netcheck 输出:
Nearest DERP: San Francisco (202.8ms)
tshttpproxy: using proxy "http://127.0.0.1:7890" for URL: "https://controlplane.tailscale.com/"
真相是 ClashX 系统代理把 tailscale 拉 DERPMap 的请求也代理走了,从美国 VPS 出去测延迟 —— 于是 tailscale 判定 SFO 最近,所有中继流量绕地球一圈。
治标(让流量走直连)+ 治本(自建 DERP)一起上。
5.2 客户端流量分流(治标)
改 s-ui 的 subClashExt,在 rules 最前面插入直连规则:
rules:
# 让 tailscale 相关流量绕过代理
- PROCESS-NAME,tailscaled,DIRECT
- PROCESS-NAME,tailscale,DIRECT
- DOMAIN-SUFFIX,tailscale.com,DIRECT
- DOMAIN,controlplane.tailscale.com,DIRECT
- DOMAIN-SUFFIX,derp.tailscale.net,DIRECT
# 自家域名直连
- DOMAIN-SUFFIX,example.com,DIRECT
- DOMAIN-SUFFIX,example.net,DIRECT
- DOMAIN-SUFFIX,corp.example,DIRECT
- DOMAIN-SUFFIX,team.example,DIRECT
- DOMAIN-SUFFIX,svc.example.io,DIRECT
# tailnet 内 IP 直连
- IP-CIDR,100.64.0.0/10,DIRECT,no-resolve
- IP-CIDR,fd7a:115c:a1e0::/48,DIRECT,no-resolve
# ...其余规则
- MATCH,🐟 漏网之鱼
陷阱:Clash rules 按顺序匹配,**
MATCH之后写的任何规则永远不会命中**。之前的 direct 规则被写在MATCH后面导致完全失效。这类坏顺序问题定期扫一下。
5.3 自建 DERP 的配置(治本)
Headscale 内建 embedded DERP,只要在 config.yaml 里打开:
derp:
server:
enabled: true
region_id: 901 # hk_main = 901, fn-NAS = 902
region_code: "hkmain"
region_name: "hk_main (HK)"
stun_listen_addr: "0.0.0.0:3478"
private_key_path: /var/lib/headscale/derp_server_private.key
automatically_add_embedded_derp_region: true
urls: [] # 关掉 tailscale 官方 DERP
paths: [] # 不用外部 derp.yaml
auto_update_enabled: false
update_frequency: 24h
fn-NAS 侧的差异只在 region_id/code/name 三处,STUN 端口都是内部 3478,但 Lucky 外部转发的公网端口不同:
- hk_main:直接暴露
UDP 3478(阿里云 ECS) - fn-NAS:Lucky 里外部
UDP 3578转发到内网UDP 3478(因为 fn-NAS 本机 3478 已被占用)
踩过的坑 1:derp.paths 期望的是 YAML 格式,不是 JSON。而且 YAML 的 map key 必须是 int(901: 不能写 "901":),否则 headscale 直接报错。
踩过的坑 2:家里是纯 IPv6 公网,hk_main 只有 IPv4,两台之间没共同 IP 族,v4 ↔ v6 之间无法互 STUN。但这不影响真实使用 —— tailscale 客户端本身能拿到两个 region 的 DERPMap,各自打洞。
踩过的坑 3:Lucky 转发规则的传输类型必须勾对。STUN 只需要 UDP,家里 v6 公网就勾 udp6;有 v4 公网时勾 udp4。tcp4/tcp6 对 STUN 无意义。
5.4 端口清单(异地办公场景照抄)
| 服务 | 端口 | 协议 | 说明 |
|---|---|---|---|
| Headscale HTTP | 8080 | TCP | 容器内,nginx 反代 |
| Nginx HTTPS | 443 / 4433 | TCP | 对外,控制面 + DERP HTTPS 中继 |
| DERP STUN | 3478 / 3578 | UDP | 打洞探测,必须放行 |
| Authelia | 内部 | TCP | 走 nginx auth_request,不对外 |
| Headplane | 3000 | TCP | 容器内,nginx /admin/ 反代 |
阿里云安全组 / 家里路由器 / Lucky 三处都要放行。iptables 加 UDP 3478 白名单要注意:如果原策略是 ACCEPT,加入规则可能触发 fw-setup.sh 把 policy 改成 DROP 影响其他服务,先 iptables -L INPUT 看 policy 再动。
六、DB 单向同步:三端多活的核心
三端多活并不意味着”随便哪台都能改”—— 那样会有分裂脑。约定:hk_main 是唯一写入端,DSM 和 fn-NAS 只读镜像。
同步脚本 scripts/sync_headscale_hk_to_fn.sh 和 scripts/sync_headscale_hk_to_dsm.sh 结构一致:
1. 在 hk_main 上用 sqlite3 .backup 做热快照到 tar.gz
2. scp 到本机中转
3. scp 到目标机
4. 目标机 docker stop headscale
5. mv 旧 db.sqlite → 备份目录
6. tar 解压到目标位置
7. 清理 db.sqlite-wal / db.sqlite-shm(这两个是 WAL 模式的残留,不清会有 checksum 不一致警告)
8. docker start headscale
9. headscale nodes list 校验节点数与源端一致
关键点:
- **必须用
sqlite3 .backup而不是直接cp**:热备份是原子的,避免拷到写一半的页。 - DSM 的 SSH 环境比较特殊:非交互式 SSH 的
PATH里没docker,必须用绝对路径/usr/local/bin/docker。 - DSM 端口是 9000 不是 22:
DSM_SSH=root@nas.example.net DSM_PORT=9000。 - sync 之后 cookie_secret 不受影响:cookie_secret 在
headplane/config.yaml里各机独立,DB 不涉及。API key 因为在 DB 里,所以自动统一。
日常工作流:
# 在 hk_main 的 Headplane UI / 或 CLI 做变更(加用户、改 ACL、批准 route)
docker exec headscale headscale users create alice
docker exec headscale headscale policy set -f /etc/headscale/acl.hujson
# 一键推到备端
./scripts/sync_headscale_hk_to_fn.sh
./scripts/sync_headscale_hk_to_dsm.sh
七、客户端 profile 秒切:switch_headscale.sh
tailscale 客户端支持多 profile,每个 profile 对应一个 login-server。首次登录后 profile 常驻本地,之后切换只需一条命令(几乎零延迟)。
7.1 首次登录(一台机器只要做一次)
# 香港 profile
sudo tailscale login \
--login-server https://headscale.example.com \
--nickname hs-hk \
--accept-routes --accept-dns \
--advertise-routes=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 # 公司机才需要
# fn-NAS profile
sudo tailscale login \
--login-server https://headscale.home.example.net:4433 \
--nickname hs-fn \
--accept-routes --accept-dns
# DSM profile
sudo tailscale login \
--login-server https://headscale.nas.example.net:4433 \
--nickname hs-dsm \
--accept-routes --accept-dns
登录会打印一个 /register/<key> 授权 URL,浏览器打开后(如果 /register/ 被 Authelia 保护,会先跳 2FA)在 Headplane 里点批准就好。
7.2 日常切换
bash scripts/switch_headscale.sh hk # 切香港控制面
bash scripts/switch_headscale.sh fn # 切飞牛 NAS
bash scripts/switch_headscale.sh dsm # 切群晖
bash scripts/switch_headscale.sh status # 查看当前
脚本内部就是 tailscale switch <profile-id>,加上:
- 自动从
~/.config/switch-headscale.conf读ADVERTISE_ROUTES并tailscale set --advertise-routes ...(家里 Mac 不设置就默认不宣告,公司 Mac 设为公司内网段) - 校验 profile 存在性;不存在时打印首登指引
- 优先使用
/opt/homebrew/bin/tailscale(避开 Mac App Store 版留下的/usr/local/bin/tailscalestub —— 那个 stub 指向/Applications/Tailscale.app,只装 brew 版会执行报No such file,静默出空输出,脚本里的 python 一json.load(空)就崩)
7.3 profile 之间 machine key 是共享的
重要认知:同一台机器切不同 profile,tailscale 的 machine key 不变(存在 /Library/Tailscale/tailscaled.state 里)。所以:
- 换服务端不用重新在 UI 里批准这台机器
- 之前批准过的 routes 状态 (
Enabled=true) 跟 machine key 走,切回来立刻可用 - 但是 prefs(含
AdvertiseRoutes)是每 profile 独立的 —— 这就是为什么早期版本脚本漏掉--advertise-routes会导致每次切 profile 都要重批准
八、如何添加”新设备”
以家里刚买的一台 Mac mini 为例,全流程:
8.1 装 tailscale 客户端
brew install --cask tailscale # macOS
# 或 Linux: curl -fsSL https://tailscale.com/install.sh | sh
8.2 首登三个 profile
按 §7.1 跑三次(hk / fn / dsm),每次浏览器点授权即可。
8.3 配置默认 profile & 路由宣告(可选)
mkdir -p ~/.config
cat > ~/.config/switch-headscale.conf <<'EOF'
# 家里机器不宣告任何路由
ADVERTISE_ROUTES=""
EOF
公司 Mac 则:
ADVERTISE_ROUTES="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
8.4 因为 ACL 有 autoApprovers,路由无需人工批准
只要该 machine 归属 alice@ user 并宣告的是 10/8 172.16/12 192.168/16 之一,Headscale 会自动 Enabled=true。切换 profile 后如果 route is not available on node,检查两处:
tailscale status里AdvertiseRoutes是否有值 —— 无则说明 profile 首登时忘加,重跑switch_headscale.sh让它自动补tailscale set --advertise-routes- Headplane /
headscale routes list里Advertised是否 true
8.5 首登时最常见的坑
- 卡在 login 无输出:多半是 login-server 网络不通。另开终端
curl -v --max-time 5 https://headscale.xxx/health,返回{"status":"pass"}才说明能到。 - profile 里出现
default或其他非hs-*昵称:老 profile 遗留,tailscale switch --list找到后tailscale switch remove <id>干掉,避免脚本误 adopt。
九、如何添加”新用户”
以给家人加账号 family 为例:
9.1 建 user
# 只在 hk_main 上做
docker exec headscale headscale users create family
docker exec headscale headscale users list
9.2 更新 ACL 让 family 也享受 autoApprovers(可选)
改 hk_main/data/headscale/config/acl.hujson:
{
"acls": [
{ "action": "accept", "src": ["*"], "dst": ["*:*"] }
],
"autoApprovers": {
"routes": {
"10.0.0.0/8": ["alice@", "family@"],
"172.16.0.0/12": ["alice@", "family@"],
"192.168.0.0/16": ["alice@", "family@"]
}
}
}
或者建 group:
"groups": {
"group:admin": ["alice@", "family@"]
},
"autoApprovers": {
"routes": {
"10.0.0.0/8": ["group:admin"],
...
}
}
推送 policy:
docker exec headscale headscale policy set -f /etc/headscale/acl.hujson
9.3 家人在自己机器上登录
生成 preauth key(免浏览器授权):
docker exec headscale headscale preauthkeys create --user family --expiration 24h --reusable家人执行:
sudo tailscale login \ --login-server https://headscale.example.com \ --nickname hs-hk \ --authkey <上一步生成的 key> \ --accept-routes --accept-dns
9.4 把变更同步到备端
./scripts/sync_headscale_hk_to_fn.sh
./scripts/sync_headscale_hk_to_dsm.sh
十、多活切换的实际处理
10.1 平时:三端并行,客户端选一个用
三端各自跑,各自的控制面 URL 都可访问。客户端只连当前 profile 指向的那一台,其他两台是”热备”。DB 每次做变更后手动 sync 一次(或加 cron 定期 sync)。
10.2 主机 hk_main 故障,切到 fn-NAS
bash scripts/switch_headscale.sh fn
machine key 不变,Enabled=true 状态在 fn-NAS 的 DB 里也有(因为 sync 过),路由立刻可用。唯一影响:故障期间对 headscale 的写操作(加机器、改 ACL)不能做,等 hk_main 恢复再统一在主上做。
10.3 家里断电,只剩 hk_main 和 DSM 可用
DSM 独立部署在群晖上,走公网 DDNS nas.example.net:4433。fn-NAS 断电不影响 DSM。切 dsm profile 即可继续。
10.4 手动”提升”备端为主
如果 hk_main 长时间不能恢复,需要允许 DSM 或 fn-NAS 接受写入:
- 停掉从 hk 的 sync 脚本(如果有 cron)
- 在新主上正常做 CLI 操作
- 事后按新主重建 sync 方向(把
sync_headscale_hk_to_*.sh改成sync_headscale_dsm_to_*.sh) - hk_main 恢复后作为备端,从新主 sync 回来一次
日常没必要走这步,等主端恢复通常比重建同步方向快。
10.5 DERP 中继的”多活”
因为 hk_main 和 fn-NAS 各自跑一份 DERP,客户端切到哪个 profile 就用哪个 region 的 DERPMap:
- 切
hs-hk→ 只用 region 901 (hkmain) - 切
hs-fn→ 只用 region 902 (fnnas)
即使某一端的 DERP 挂了,切另一个 profile 就能继续中继(当然 P2P 通常也能打通,DERP 只是兜底)。
十一、常用命令速查
# ── Headscale 状态 ──
docker exec headscale headscale nodes list
docker exec headscale headscale users list
docker exec headscale headscale routes list
docker exec headscale headscale apikeys list
# ── 生成 preauth key / API key ──
docker exec headscale headscale preauthkeys create --user alice --expiration 24h
docker exec headscale headscale apikeys create -e 87660h
# ── 手动批准 route(正常有 autoApprovers 不需要) ──
docker exec headscale headscale routes enable -r <route_id>
# ── 修改并加载 ACL ──
vi hk_main/data/headscale/config/acl.hujson
docker exec headscale headscale policy set -f /etc/headscale/acl.hujson
# ── 客户端 ──
tailscale switch --list
tailscale status
tailscale netcheck | head -20 # 看 Nearest DERP 是否是自建
tailscale ping --verbose <peer-name> # 看是 direct 还是 DERP 中继
# ── 三端 DB 同步 ──
./scripts/sync_headscale_hk_to_fn.sh
./scripts/sync_headscale_hk_to_dsm.sh
# ── Headplane 升级(hk → fn/dsm 中转) ──
./scripts/upgrade_headplane.sh 0.7.2 # DRY_RUN=1 演练
十二、踩坑合集
- 0.23 → 0.29 升级:
autoApprovers.routes的 owner 必须是 typed entity (alice@/group:/tag:),光写"alice"会启动失败。 - **
ephemeral_node_inactivity_timeout**:0.29 迁到node.ephemeral.inactivity_timeout,老写法只 WARN 不阻塞,但升级时一并改掉更清爽。 - DERP
paths是 YAML 不是 JSON,且 map key 必须 int。 - Lucky UDP 转发的传输类型:家里 v6 公网只勾
udp6;有 v4 公网加udp4;tcp4/tcp6对 STUN 无意义。 - **iptables policy 是
ACCEPT时不要乱加-A INPUT**:某些自动化脚本会顺带把 policy 改成DROP,会破坏其他服务。 - 切 profile 后路由消失:脚本必须在 switch 后
tailscale set --advertise-routes,因为 prefs 是每 profile 独立的。 - Mac 上
/usr/local/bin/tailscale是 App Store stub:只装 brew 版时它会静默失败,脚本要显式用/opt/homebrew/bin/tailscale。 - Authelia 改了配置不重启:请求匹配不到规则会返回 403 而不是 401。
docker restart authelia是必做步骤。 goodieshq/headscale-adminv0.28.0 只支持 headscale ≤0.25:0.29 上直接白屏,换 Headplane。- ClashX 代理污染 tailscale:
tshttpproxy: using proxy ...出现在 netcheck 里说明流量被系统代理拦了。要么让 tailscale 走PROCESS-NAMEDIRECT,要么调整 rules 里的 domain suffix。
十三、未来可以做的事
- cron 定时跑
sync_headscale_hk_to_*.sh(现在是手动,容易忘) - Headplane 的
cookie_secret和api_key改成 docker secrets 注入,脱离 git 明文 -
headplane 0.7.x → 0.8.x升级路径试跑 - 给 fn-NAS 的 DERP 加 IPv4 出口(家里 v6 only 场景下 v4-only 客户端只能走 hk region)
- 加一台海外备份控制面(比如 vmiss-us),做 3+1 多活
十四、小结
从”想让家里 Mac ping 通公司 IP”这个 30 分钟需求,一路挖到最后大概花了三个夜晚,收获是把 控制面 / 管理面 / 数据面 / 客户端流量分流 / 多端 DB 同步 这一整套解耦清楚。核心心得:
- 控制面(Headscale)、管理面(Headplane)、数据面(DERP)尽量各自独立部署,只在必要处共享(DB / API key)。
- **多活的关键不是”双主”,是”单写多读 + 秒切”**。真需要故障切主,脚本改一下方向即可。
- 每一层安全都只做一件事:Authelia 管浏览器 UI 的身份、Headscale API Key 管机器 API、ACL autoApprovers 管路由授权、DERP 只转发加密流量。
- 踩坑一定要写下来。三个月后再遇到同样错误,能省两小时。
(本文将持续更新。)