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。


20260418-RAG-khoj-my-second-AI-brain

安装并使用khoj RAG系统接入智谱AI作为第二AI大脑

使用docker-compose方式部署khoj

根据khoj官方文档

1
2
3
mkdir /opt/stacks/khoj
cd /opt/stacks/khoj
wget https://raw.githubusercontent.com/khoj-ai/khoj/master/docker-compose.yml

然后修改模板文件中的相关配置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
environment:
- KHOJ_DJANGO_SECRET_KEY=KHOJ-DJANGO_STRONG_RANDOM_PASSWORD
# admin panel email / password
- KHOJ_ADMIN_EMAIL=admin_email@home.lab
- KHOJ_ADMIN_PASSWORD=KHOJ-ADMIN-STRONG-PASSWORD
# 设置khoj_domain用来指定我们在浏览器中访问时,需要输入的网址、域名
- KHOJ_DOMAIN=khoj.home.lab
# 不使用https
- KHOJ_NO_HTTPS=True
# 智谱 GLM, 兼容OPENAI接口 {{{1
- OPENAI_BASE_URL=https://open.bigmodel.cn/api/paas/v4
- OPENAI_API_KEY=XXXX_zhipu_api_key_YYY
# “高性价比” 基座模型,价格是 0.5 元 / 百万 Tokens
#- KHOJ_DEFAULT_CHAT_MODEL=GLM-4-Air-250414
# - KHOJ_DEFAULT_CHAT_MODEL=glm-4.5-air
# “轻量高速”,并明确写了它适用于 中文写作、翻译、长文本等通用场景,上下文是 200K
# - KHOJ_DEFAULT_CHAT_MODEL=GLM-4.7-FlashX
- KHOJ_DEFAULT_CHAT_MODEL=glm-4.7
# }}}
# 并且修改启动参数,删除匿名模式,不允许匿名访问
command: --host="0.0.0.0" --port=42110 -vv -non-interactive
1
2
3
4
5
6
7
# 启动 khoj docker服务
docker compose up -d

# 查看服务日志
docker compose logs -f

docker compose restart

配置search model

进入 khoj_admin_panel,默认为http://${KHOJ_DOMAIN}:42110/server/admin;
作为管理本地markdown笔记文档来说,按以下对Search model进行调优。 功能菜单Search model configs,添加一个新的名为default的search model, khoj只会使用第一个名为default的search model;在新建model之前先记录已经存在的Model的id号。 便于区分新建model。按以下参数新建search model,建立完成后删除名为default名的所有旧model; 然后重启 khoj服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# khoj只会使用第一个名为default的search model
name: default

# khoj明确把它作为非英文文档的示例推荐,说明它支持 50+ 语言,并且在消费级机器上有不错的速度和效果
# 对多语言支持,默认的bi_encoder仅支持英文文档;对于中英混合文档使用:
bi_encoder: sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2

# 对 bi-encoder 召回出的文档做更高质量的重排序
cross_encoder: mixedbread-ai/mxbai-rerank-xsmall-v1

bi_encoder_query_encode_config: {}

# 0.0 更接近“几乎完全重合”
# 1.0 更接近“几乎没有语义重合”
# 0.2~0.25: 更严格,对于我们自己提供的文档有要求,需要文档主题清晰,问题是可能太严格,导致漏召回
# 0.35~0.45: 更宽松,适合我们自己提供的文档写法随意,关键词不固定,,问题是可能匹配的文档太多,噪声太大
bi_encoder_confidence_threshold: 0.30
1
docker compose restart

普通用户使用

访问http://${KHOJ_DOMAIN}:42110可以进入khoj登录页面,直接输入邮箱,然后在ADMIN_PANEL的 功能菜单Users-选中用户-Get Email Login Url,复制URL后,直接从浏览器访问即可; 如此方式在homelab方式下即不会有匿名访问问题,也不需要配置其它登录方式。

配置agents

由于普通用户身份创建agent可能会进入卡死循环,使用管理员面板创建agent。

功能菜单Agents,新建agent;最重要的是prompt;document可以稍后由用户提供

提供documents

使用官方GUI Desktop同步工具 配置URL和用户API后选择文件夹进行同步。同步需要比较长时间, 同步实际完成后用户下功能菜单Search下文档已经全部出现;设置agents附加下所有相关文件。

khoj agents search

智谱BIG-MODEL邀请

我正在智谱大模型开放平台 BigModel.cn上打造AI应用,智谱新一代旗舰模型GLM-5已上线, 在推理、代码、智能体综合能力达到开源模型 SOTA 水平,通过我的邀请链接注册即可获得 2000万Tokens 大礼包,期待和你一起在BigModel上畅享卓越模型能力;链接: