20260915-headscale-caddy-openwrt-homelab
自建 Headscale:Caddy 反代 + OpenWrt 子网路由打通家宽 Homelab
用官方 Tailscale 当客户端、自建 Headscale 当控制面,把家里的 10.10.0.0/16 挂进 tailnet。控制面跑在阿里云上海 ECS,域名 hola.holderserver.lab,MagicDNS 后缀 ts.holderserver.lab。
目标架构
1 | Windows / 手机(任意网络) |
角色分工:
| 角色 | 机器 | 作用 |
|---|---|---|
| 控制面 + DERP | 大陆 ECS | Headscale 0.29.3 + Caddy 签证书、反代、中继 |
| 子网路由 | 家里 OpenWrt | 宣告 10.10.0.0/16,转发 tailnet ↔ LAN |
| 日常客户端 | Windows | 下班/手机热点访问家里服务 |
要点:
- Headscale 容器内只听
8080,不要映射到宿主机 8080。宿主机 80/443 给 Caddy,3478/udp 给 STUN。 prefixes.v4/v6是 tailnet 虚拟地址池(100.64.0.0/10与fd7a:115c:a1e0::/48),和 VPS 有没有公网 IPv6、家里是不是10.10.0.0/16无关。server_url主机名必须和 MagicDNSbase_domain不同:这里分别是hola.holderserver.lab与ts.holderserver.lab。- 0.29.3 要求 Tailscale 客户端 ≥ 1.80。
目录与 Compose
VPS 上目录:
1 | /opt/stacks/headscale/ |
compose.yml:
1 | services: |
拉 headscale/headscale:0.26.4 时若出现:
1 | Get "https://registry-1.docker.io/v2/": net/http: request canceled |
直接换 GHCR 或加速前缀,版本用当时的 release 0.29.3,不要死盯旧 tag。
80 若被系统 Apache 占用:
1 | sudo systemctl stop apache2 |
Headscale 配置
config/config.yaml(0.29 可用字段):
1 | server_url: https://hola.holderserver.lab |
说明:
derp.urls: []表示不用官方 DERP,打洞失败时流量只走自己的 VPS。region_id/region_code/region_name只是 DERP 地图里的显示名,和连通性无关;region_id有节点连上后不要乱改。split.home.lab → 10.10.1.1用来解析家里自建域名(见文末 DNS)。没有 Homelab 域名可先去掉split。listen_addr: 0.0.0.0:8080只绑在 Headscale 容器网栈 里。另一台容器也可以在自己内部听 8080,只要不要都-p 8080:8080到同一台宿主机。
默认全通 ACL config/acl.hujson:
1 | { |
Caddyfile
能稳定跑起来的版本:
1 | hola.holderserver.lab { |
flush_interval -1 减少 /machine/map 长轮询被缓冲。
不要把 read_timeout 直接写在 reverse_proxy 块里。当前 caddy:2 会报:
1 | unrecognized subdirective read_timeout |
容器随即 Restarting (1),宿主机 443 变成 connection refused,所有客户端立刻 logged out。超时参数必须写在该版本认的 transport http 语法里;不确定就维持上面这份最小配置。
改完:
1 | docker compose up -d |
Headscale 正常启动会看到 listening and serving HTTP on: 0.0.0.0:8080,以及:
1 | WRN listening without TLS but ServerURL does not start with http:// |
这是预期:TLS 在 Caddy 上终结,Headscale 只对内提供 HTTP。
证书与 80/443 打不通
Caddy 本机回环可以 308 到 HTTPS,但外网 curl http://108.108.108.108/ 超时,Let’s Encrypt 会报:
1 | Timeout during connect (likely firewall problem) |
分层排查:
UFW 的 INPUT 不够。 装了
ufw-docker之后,容器映射端口走 FORWARD。下面这种规则看起来开了 80/443,其实拦不到 Docker:1
2
380/tcp ALLOW IN Anywhere
443/tcp ALLOW IN Anywhere
8080/tcp ALLOW FWD 特定 IP需要:
1
2
3
4
5
6
7
8sudo ufw-docker allow caddy 80/tcp
sudo ufw-docker allow caddy 443/tcp
sudo ufw-docker allow headscale 3478/udp
# 或
sudo ufw route allow proto tcp from any to any port 80
sudo ufw route allow proto tcp from any to any port 443
sudo ufw route allow proto udp from any to any port 3478
sudo ufw reloadufw status里应出现 ALLOW FWD 的 80/443/3478。不要去放行 Headscale 的 8080。阿里云安全组 入方向 TCP 80、TCP 443、UDP 3478,源
0.0.0.0/0,且规则挂在拥有108.108.108.108的那张网卡上。开过「云防火墙」还要再放一刀。ECS备案 80、443端口需要备案。
成功日志特征:
1 | served key authentication certificate challenge=tls-alpn-01 |
验证:
1 | curl -I https://hola.holderserver.lab |
curl -I 发的是 HEAD,Headscale 根路径常回 HTTP/2 405 Allow: GET,说明反代和证书都好,不是服务挂了。改用 GET 或打 /health。
浏览器打开该域名多半是空白/404,Headscale 没有管理后台首页。
用户、Key 与 0.29 CLI
1 | docker exec -it headscale headscale users create default |
0.29 起 --user 必须是 数字 ID,再写 default 会:
1 | strconv.ParseUint: parsing "default": invalid syntax |
路由也不再是 headscale routes:
1 | docker exec -it headscale headscale nodes list |
批准后三列都要有网段:
1 | ID | Hostname | Approved | Available | Serving (Primary) |
preauth key 只显示一次。可以手动将key过期
1 | docker exec -it headscale headscale preauthkeys list --user 1 |
OpenWrt 当子网路由
环境:eSir R24.2.2 / 内核 5.15.151 / J4125,LAN:
1 | br-lan inet addr:10.10.1.1 Mask:255.255.0.0 |
即网段 10.10.0.0/16,宣告时写网段不要写成 10.10.1.1/16。
不要走坏掉的 opkg
/etc/opkg/distfeeds.conf 指向了已经 404 的 snapshots,还混着 18.06.9 的 luci,wget returned 8。定制固件不要强行对官方 24.10 源,直接下官方静态包。
1 | cd /tmp |
需要 ≥ 1.80,这里是 1.92.5。
OpenWrt 没有 systemd,用 procd:
1 | cat > /etc/init.d/tailscale << 'EOF' |
接入:
1 | tailscale up \ |
之后重连一般 不必再带 authkey,状态在 tailscaled.state 里。只有 logged out、删过 state、或节点在服务端被删时才需要 key。
zsh 下改防火墙
opwnwrt下 shell在更改为 zsh的情况下,firewall.@zone[-1] 的方括号会被当成通配符:
1 | zsh: no matches found: firewall.@zone[-1].name=tailscale |
结果 uci add 建出无名 zone,firewall restart 出现 Section @zone[4] has no name。所有带 [ 的参数加单引号:
1 | uci add firewall zone |
成功时日志里有:
1 | Zone 'tailscale' |
uci commit 写入 /etc/config/firewall,重启还在。sysctl -w net.ipv4.ip_forward=1 只改当前内核,OpenWrt 默认通常已开转发。
这台机还跑着 OpenClash / Passwall。本机访问 hola.holderserver.lab 必须直连,否则会出现 DERP RST、PollNetMap 超时。先:
1 | curl -v --max-time 15 https://hola.holderserver.lab/ |
短请求 200 只说明 TCP/TLS 通,不能排除长轮询被掐。
Windows 客户端
装官方 Tailscale,不要找 Headscale Windows 版。管理员 PowerShell:
1 | tailscale up --login-server https://hola.holderserver.lab --authkey "hskey-auth-..." --accept-routes --accept-dns |
不要点默认 Log in(那会进官方云)。
验证:
1 | tailscale status |
100.64.0.2 是 Windows 自己 的 tailnet 地址,ping 自己 <1ms 没有诊断意义。
100.64.0.1/openwrt:路由器在 tailnet 里的地址10.10.1.1和家里 NAS 的10.10.x.x:子网路由
不需要固定公网 IP。Wi-Fi、网线、手机热点、4G/5G 都能用;运营商 NAT 下多半走 DERP(hola.holderserver.lab:443),延迟略高。
每天关机:Tailscale 是 Windows 服务,默认会记住 --login-server。开机等托盘 Connected 即可,不要每次 logout。服务设为 Automatic,并:
1 | tailscale set --accept-routes --accept-dns |
只有变成 logged out 才重新带 key。早上若不通,先看 OpenWrt tailscale status 和 VPS 上 Caddy 是否还是 Up。
Homelab 域名
远程时客户端 DNS 来自 Headscale(全局 223.5.5.5),不会问家里的 dnsmasq,所以 openwrt.home.lab 解析不了,只能打 IP。
在 dns.nameservers.split 把 home.lab 指到 10.10.1.1,改完 docker compose restart headscale,Windows:
1 | tailscale down |
若 REFUSED,让 dnsmasq 回答来自 tailnet / SNAT 后的查询(--snat-subnet-routes 时源地址常常已经是 10.10.1.1)。必要时:
1 | uci set dhcp.@dnsmasq[0].localservice='0' |
两套名字:
| 名字 | 解析方 | 结果 |
|---|---|---|
openwrt / openwrt.ts.holderserver.lab |
MagicDNS | 100.64.0.1 |
openwrt.home.lab |
家里 DNS(split) | 10.10.1.1 |
主机不多也可以用 dns.extra_records 写死 A 记录。
排障备忘
| 现象 | 常见原因 |
|---|---|
| 外网 80/443 超时,本机 curl 308 | ufw-docker 未 ALLOW FWD / 安全组 / 云防火墙 |
Caddy Restarting (1),客户端 connection refused |
Caddyfile 语法错误(如乱写 read_timeout) |
OpenWrt PollNetMap / long-poll timed out |
反代空闲断开,或 Clash 劫持本机 443 |
preauthkeys --user default 失败 |
0.29 要用户 ID |
headscale routes unknown |
改用 nodes list-routes / approve-routes |
| OpenWrt opkg snapshot 404 | 换官方 amd64 静态包 |
uci no matches found |
zsh 吃掉 [],参数加引号 |
ping 通 100.64.0.1 不通 10.10.1.1 |
路由未批准或防火墙无 tailscale ↔ lan |
Windows ping 100.64.0.2 很快 |
那是本机地址 |
Caddy 挂掉时 OpenWrt 典型日志:
1 | Get "https://hola.holderserver.lab/key?v=131": dial tcp 108.108.108.108:443: connect: connection refused |
先修 VPS docker compose ps,再在路由器上 tailscale up。
日常维护
1 | # VPS |
扩容就是再发一条 preauth key,新设备 --login-server https://hola.holderserver.lab。只有要进家宽的客户端才开 --accept-routes。