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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Windows / 手机(任意网络)
│ Tailscale 客户端
│ --login-server https://hola.holderserver.lab
▼
┌─────────────────────────────────────────┐
│ 阿里云 ECS 108.108.108.108 │
│ Caddy :80/:443 ──反代──► Headscale :8080 │
│ Headscale 内嵌 DERP + STUN :3478/udp │
└─────────────────────────────────────────┘
│
▼
OpenWrt(x86 J4125,10.10.1.1/16)
tailscale 子网路由 advertise 10.10.0.0/16
│
▼
家宽设备 10.10.x.x / openwrt.home.lab

角色分工:

角色 机器 作用
控制面 + 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 主机名必须和 MagicDNS base_domain 不同:这里分别是 hola.holderserver.lab 与 ts.holderserver.lab。
  • 0.29.3 要求 Tailscale 客户端 ≥ 1.80。

目录与 Compose

VPS 上目录:

1
2
3
4
5
6
/opt/stacks/headscale/
compose.yml
Caddyfile
config/config.yaml
config/acl.hujson
data/

compose.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
services:
caddy:
image: caddy:2
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
depends_on:
- headscale
networks:
- hsnet

headscale:
# Docker Hub 在国内经常超时,走 GHCR 或国内镜像
image: ghcr.io/juanfont/headscale:0.29.3
# 国内加速镜像:ghcr.1ms.run/juanfont/headscale:0.29.3
container_name: headscale
restart: unless-stopped
command: serve
volumes:
- ./config:/etc/headscale
- ./data:/var/lib/headscale
ports:
- "3478:3478/udp"
networks:
- hsnet

networks:
hsnet:

volumes:
caddy-data:
caddy-config:

拉 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
2
3
sudo systemctl stop apache2
sudo systemctl disable apache2
sudo systemctl mask apache2

Headscale 配置

config/config.yaml(0.29 可用字段):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
server_url: https://hola.holderserver.lab
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090
grpc_listen_addr: 127.0.0.1:50443
grpc_allow_insecure: false

noise:
private_key_path: /var/lib/headscale/noise_private.key

prefixes:
v4: 100.64.0.0/10
v6: fd7a:115c:a1e0::/48
allocation: sequential

derp:
server:
enabled: true
region_id: 900
region_code: "aliyun_sh"
region_name: "Aliyun Shanghai"
stun_listen_addr: "0.0.0.0:3478"
private_key_path: /var/lib/headscale/derp_server_private.key
verify_clients: true
ipv4: "108.108.108.108"
# 无公网 IPv6 就不写 ipv6
urls: []
auto_update_enabled: false

dns:
magic_dns: true
base_domain: ts.holderserver.lab
override_local_dns: true
nameservers:
global:
- 223.5.5.5
split:
# 家庭dns 拆分
home.lab:
- 10.10.1.1

database:
type: sqlite
sqlite:
path: /var/lib/headscale/db.sqlite

log:
level: info
format: text

unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"

policy:
mode: file
path: /etc/headscale/acl.hujson

说明:

  • 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
2
3
4
5
6
7
8
9
10
11
12
{
"groups": {
"group:admin": []
},
"acls": [
{
"action": "accept",
"src": ["*"],
"dst": ["*:*"]
}
]
}

Caddyfile

能稳定跑起来的版本:

1
2
3
4
5
6
hola.holderserver.lab {
encode gzip
reverse_proxy headscale:8080 {
flush_interval -1
}
}

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
2
3
4
docker compose up -d
docker compose ps
docker compose logs --tail=80 caddy
docker compose logs --tail=80 headscale

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)

分层排查:

  1. UFW 的 INPUT 不够。 装了 ufw-docker 之后,容器映射端口走 FORWARD。下面这种规则看起来开了 80/443,其实拦不到 Docker:

    1
    2
    3
    80/tcp     ALLOW IN     Anywhere
    443/tcp ALLOW IN Anywhere
    8080/tcp ALLOW FWD 特定 IP

    需要:

    1
    2
    3
    4
    5
    6
    7
    8
    sudo 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 reload

    ufw status 里应出现 ALLOW FWD 的 80/443/3478。不要去放行 Headscale 的 8080。

  2. 阿里云安全组 入方向 TCP 80、TCP 443、UDP 3478,源 0.0.0.0/0,且规则挂在拥有 108.108.108.108 的那张网卡上。开过「云防火墙」还要再放一刀。

  3. ECS备案 80、443端口需要备案。

成功日志特征:

1
2
served key authentication certificate  challenge=tls-alpn-01
certificate obtained successfully identifier=hola.holderserver.lab

验证:

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
2
3
docker exec -it headscale headscale users create default
docker exec -it headscale headscale users list
docker exec -it headscale headscale preauthkeys create --user 1 --reusable --expiration 24h

0.29 起 --user 必须是 数字 ID,再写 default 会:

1
strconv.ParseUint: parsing "default": invalid syntax

路由也不再是 headscale routes:

1
2
3
docker exec -it headscale headscale nodes list
docker exec -it headscale headscale nodes list-routes
docker exec -it headscale headscale nodes approve-routes --identifier 1 --routes 10.10.0.0/16

批准后三列都要有网段:

1
2
ID | Hostname | Approved     | Available    | Serving (Primary)
1 | openwrt | 10.10.0.0/16 | 10.10.0.0/16 | 10.10.0.0/16

preauth key 只显示一次。可以手动将key过期

1
2
docker exec -it headscale headscale preauthkeys list --user 1
docker exec -it headscale headscale preauthkeys expire --user 1 <key-id>

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
2
3
4
5
6
7
cd /tmp
wget https://pkgs.tailscale.com/stable/tailscale_1.92.5_amd64.tgz
tar -xzf tailscale_1.92.5_amd64.tgz
cp tailscale_1.92.5_amd64/tailscale tailscale_1.92.5_amd64/tailscaled /usr/sbin/
chmod +x /usr/sbin/tailscale /usr/sbin/tailscaled
mkdir -p /var/lib/tailscale /var/run/tailscale
tailscale version

需要 ≥ 1.80,这里是 1.92.5。

OpenWrt 没有 systemd,用 procd:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cat > /etc/init.d/tailscale << 'EOF'
#!/bin/sh /etc/rc.common
START=99
STOP=10
USE_PROCD=1

start_service() {
mkdir -p /var/run/tailscale /var/lib/tailscale
procd_open_instance
procd_set_param command /usr/sbin/tailscaled
procd_append_param command --state /var/lib/tailscale/tailscaled.state
procd_append_param command --socket /var/run/tailscale/tailscaled.sock
procd_set_param respawn
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}
EOF
chmod +x /etc/init.d/tailscale
/etc/init.d/tailscale enable
/etc/init.d/tailscale start

接入:

1
2
3
4
5
6
tailscale up \
--login-server https://hola.holderserver.lab \
--authkey 'hskey-auth-...' \
--advertise-routes=10.10.0.0/16 \
--accept-dns \
--snat-subnet-routes

之后重连一般 不必再带 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
uci add firewall zone
uci set 'firewall.@zone[-1].name=tailscale'
uci set 'firewall.@zone[-1].input=ACCEPT'
uci set 'firewall.@zone[-1].output=ACCEPT'
uci set 'firewall.@zone[-1].forward=ACCEPT'
uci add_list 'firewall.@zone[-1].device=tailscale0'

uci add firewall forwarding
uci set 'firewall.@forwarding[-1].src=tailscale'
uci set 'firewall.@forwarding[-1].dest=lan'

uci add firewall forwarding
uci set 'firewall.@forwarding[-1].src=lan'
uci set 'firewall.@forwarding[-1].dest=tailscale'

uci commit firewall
/etc/init.d/firewall restart

成功时日志里有:

1
2
3
Zone 'tailscale'
Forward 'tailscale' -> 'lan'
Forward 'lan' -> '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
2
3
tailscale status
ping 100.64.0.1
ping 10.10.1.1

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
2
3
4
tailscale down
tailscale up --login-server https://hola.holderserver.lab --accept-routes --accept-dns
ipconfig /flushdns
nslookup openwrt.home.lab

若 REFUSED,让 dnsmasq 回答来自 tailnet / SNAT 后的查询(--snat-subnet-routes 时源地址常常已经是 10.10.1.1)。必要时:

1
2
3
uci set dhcp.@dnsmasq[0].localservice='0'
uci commit dhcp
/etc/init.d/dnsmasq restart

两套名字:

名字 解析方 结果
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
2
Get "https://hola.holderserver.lab/key?v=131": dial tcp 108.108.108.108:443: connect: connection refused
You are logged out.

先修 VPS docker compose ps,再在路由器上 tailscale up。

日常维护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# VPS
docker compose ps
docker compose logs --tail=50 caddy
docker exec -it headscale headscale nodes list
docker exec -it headscale headscale nodes list-routes

# OpenWrt
/etc/init.d/tailscale start
tailscale status

# Windows
tailscale status
ping 100.64.0.1
ping 10.10.1.1

扩容就是再发一条 preauth key,新设备 --login-server https://hola.holderserver.lab。只有要进家宽的客户端才开 --accept-routes。


openwrt-kms-active-win-office

openwrt KMS服务器激活windows与office

参考

破浪: Openwrt的KMS服务,激活windows和office

激活过程: 在需要激活的机器上设置好KMS服务器的地址,让机器使用windows自身的激活机制进行激活。

先决条件: 安装的windows镜像、office镜像版本都是批量授权版(VOL),而不是零售版(Retail)。 区别就是批量授权版类似于供应企业使用,为了方便企业激活,允许使用KMS服务器批量激活。

Openwrt esir 高大全固件默认存在KMS服务器服务,开启即可。

windows激活

MS 官方 各版本镜像下载 ; 开启开发者工具后, 将屏幕设备修改为移动设备,即可以直接下载。

windows激活过程,按照步骤执行激活过程即可。

  • 给需要激活机器先卸载已存在密钥

    1
    slmgr /upk
  • 给需要激活机器安装对应版本密钥

    ms kms client activation keys

    1
    2
    # 专业工作站版密钥
    slmgr /ipk NRG8B-VKK3Q-CXVCJ-9G2XF-6Q84J

    esir Openwrt 固件KMS服务器配置文件中提供的几个版本的Key

    #Windows 10/ Windows 11 KMS 安装激活密钥

    #Windows 10/11 Pro:W269N-WFGWX-YVC9B-4J6C9-T83GX

    #Windows 10/11 Enterprise:NPPR9-FWDCX-D2C8J-H872K-2YT43

    #Windows 10/11 Pro for Workstations:NRG8B-VKK3Q-CXVCJ-9G2XF-6Q84J

  • 给需要激活的机器设置KMS服务器地址

    1
    2
    # openwrt ip地址
    slmgr /skms 192.168.5.1
    1
    2
    # 确认KMS服务器地址已经正常设置, 解析结果为 192.168.5.1
    nslookup -type=srv _vlmcs._tcp.lan
  • 执行手动激活

    1
    slmgr /ato

office 激活

一定要安装VOL版本office,否则激活不成功,可能有解决办法破浪: Openwrt的KMS服务,激活windows和office, 但是很麻烦。

安装VOL版本office

参照祕技: 安装部署Microsoft Office LTSC 专业增强版 2021 ,使用官方office Office部署工具(Office Deployment Tool), 进行office Vol各版本office手动安装部署。

  • 下载office deployment tool

    https://www.microsoft.com/en-us/download/details.aspx?id=49117

  • 运行office deployment tool

    创建一个空文件夹(例如名称为: office_file),在运行office deployment tool后,选择这个空文件夹。(整个安装过程完成后,此文件夹可以删除)
    运行完成后,文件夹下会生成几个office安装描述xml配置文件,和setup.exe。删除生成的几个xml配置文件。

  • 生成需要安装的对应版本office描述xml配置文件

    在MS office 官方xml配置文件生成网站中生成xml配置文件,然后下载到office_file中。 例如Office LTSC 专业增强版xml如下: 包含Visio LTSC专业版、Project LTSC专业版

    office_LTSC_pro_plus_2021.xml
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <Configuration ID="0120b23a-3fdc-43ad-8f91-84dffbff66de">
    <Add OfficeClientEdition="64" Channel="PerpetualVL2021">
    <Product ID="ProPlus2021Volume" PIDKEY="FXYTK-NJJ8C-GB6DW-3DYQT-6F7TH">
    <Language ID="zh-cn" />
    <ExcludeApp ID="Lync" />
    </Product>
    <Product ID="VisioPro2021Volume" PIDKEY="KNH8D-FGHT4-T8RK3-CTDYJ-K2HT4">
    <Language ID="zh-cn" />
    <ExcludeApp ID="Lync" />
    </Product>
    <Product ID="ProjectPro2021Volume" PIDKEY="FTNWT-C6WBT-8HMGF-K9PRX-QV9H8">
    <Language ID="zh-cn" />
    <ExcludeApp ID="Lync" />
    </Product>
    </Add>
    <Property Name="SharedComputerLicensing" Value="0" />
    <Property Name="FORCEAPPSHUTDOWN" Value="FALSE" />
    <Property Name="DeviceBasedLicensing" Value="0" />
    <Property Name="SCLCacheOverride" Value="0" />
    <Property Name="AUTOACTIVATE" Value="1" />
    <Updates Enabled="TRUE" />
    <RemoveMSI />
    </Configuration>
  • 使用setup.exe按配置文件下载office安装数据

    1
    2
    3
    4
    # 进入setup.exe所在目录
    cd office_file

    PS C:\Users\xxx\office_file> .\setup.exe /download ".\office_LTSC_pro_plus_2021.xml"

    命令完成后,office_file目录下存在下载完成的office 数据。

  • 使用setup.exe对已经下载的office安装数据进行配置安装

    1
    2
    3
    4
    # 进入setup.exe所在目录
    cd office_file

    PS C:\Users\xxx\office_file> .\setup.exe /configure ".\office_LTSC_pro_plus_2021.xml"

    命令完成后,office已经安装完毕,将各office组件,都打开一遍,准备激活。

office KMS服务器激活

  • 进入到office安装目录下

    1
    2
    # Office15, Office16
    PS > cd 'C:\program files\microsoft office\Office16'
  • 使用官方ospp.vbs脚本设置KMS服务器地址

    1
    2
    # openwrt地址 192.168.5.1
    PS C:\program files\microsoft office\Office16> cscript ospp.vbs /sethst:192.168.5.1
  • 使用官方ospp.vbs脚本手动执行激活

    1
    PS C:\program files\microsoft office\Office16> cscript ospp.vbs /act
  • 使用官方ospp.vbs脚本查看office安装key、激活状态

    1
    PS C:\program files\microsoft office\Office16> cscript ospp.vbs /dstatus