Files

253 lines
31 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HANDOFF — 部署形态与运维事实
本文承接仓库外的历史上下文,供 DSH 会话直接使用。
- **来源**:2026-08-29 → 09-28 的旧 Claude Code 会话(原件归档在 `~/.dsh/archive/vpsetup-claude-2026-09-28/`,含明文凭据,勿外传)。
- **主机状态**:以下带 ✅/⚠️ 的状态为 **2026-10-09 实测**,不是历史记录。
- **原则**:本文只写事实与结论,**不写任何凭据**(uuid / 私钥 / token / 分享链接一律不出现在仓库里)。
---
## 1. 机队清单(2026-10-09 实测)
| 主机 | 登录 | 角色 | 实测状态 |
|---|---|---|---|
| `dockerno1` = `192.168.254.50` | `eli@` | **家用服务器(国内),非代理节点**。仓库 + 旧 Claude 工作台在这里;另跑大量 docker 服务(nextcloud / collabora / firecrawl / open-webui / portainer / uptime-kuma …)。 | ✅ 正常。**没有** caddy/xray/hysteria systemd 栈;443/8080/8443 的监听来自 docker 容器,排查端口占用时别搞混。 |
| `oraclejp.henhaoji.site` | `ubuntu@` | Oracle 日本 1 号节点 | ✅ caddy/xray/hysteria-server 全 active,Xray 26.3.27,`/root/setup_caddy_xray_hysteria.sh` 常驻 |
| `oraclejp-2.henhaoji.site` | `ubuntu@` | Oracle 日本 2 号节点 | ✅ 同上 |
| `gcoregon.henhaoji.site` | `xu_co@` | GCP Oregon:**cdn-bypass 透明代理节点**(GCP 对 CDN 流量收费/受限) | ✅ 三件套 active + `cdn-bypass-route` active,dokodemo 54443 在听,nft 里有 `cdn_bypass_mark` + `cdn_bypass_tproxy` |
| `alius.henhaoji.site` | `root@` | 阿里云 US:cdn-bypass 的**上游**节点 | ✅ 三件套 active;无 cdn-bypass,无 XHTTP(8080) |
| `git.henhaoji.site` | `ubuntu@` | 私有 git 服务器(本仓库 origin) | ✅ 正常 |
| `euserv.henhaoji.site` = `2a02:180:6:1::7ca` | — | EUserv 德国 VPS,**IPv6-only**,IPv4 出口靠 `setup_warp_zerotrust.sh` 走 WARP | ⚠️ **不可达**(详见 §7) |
| Northflank 容器 | 无 SSH,纯 API | 兜底节点(template 驱动) | 见 §5 |
**从本机(WSL 笔记本)的连通性**:私钥已在位,上表有登录用户的主机全部可直连,**不需要拷贝私钥、不需要跳板**。
本机经 WSL NAT(`172.25.2.12` → 网关 `172.25.0.1`)路由进用户内网,因此 `192.168.254.50` 可达。
`bwg` / UK / `gpu` 等未列入的主机:`gpu` 本机直连(`eli@gpu`),其余可经 `ssh eli@dockerno1` 跳板。
---
## 2. 各机差异与注意点
- **Oracle 双机**:两机**凭据完全相同**(同一套 REALITY / WS / hy2 / CF token),`WS_PATH=/vlessws`,`HY2_CERT_MODE=dns`(`*.henhaoji.site` 通配证书)。橙云域名 `oraclejp-1-cf` / `oraclejp-2-cf.henhaoji.site` 已配置并端到端验证。Oracle1 主 Caddyfile 里原有一批死配置(php_fastcgi / portainer 反代,服务早已 inactive),迁移时已随旧块去除;`oraclejp.duckdns.org` 别名两机均已不再服务。
- **gcoregon**:`xu_co@` + 免密 sudo,docker inactive。部署脚本**不在**机器上常驻(部署时 scp 到 `/tmp/cdn.sh` 跑)。2026-09-28 已从手工栈迁入脚本管理(`migrate_existing_stack.sh` → `ACTION=upgrade`),随后装上 cdn-bypass。**级联验证闭环**:脚本 DRY_RUN 文本断言(`tests/run_tests.sh`)→ gcoregon 上跑的就是这份生成配置 → 线上实测出口正确。
- **dockerno1**:用户的开发/控制台机器,**不是节点**。旧 Claude Code 会话在这里运行,Session 历史与归档也在这里。
- **oraclejp**:2026-10-10 起主 Caddyfile 末尾带 `import /etc/caddy/sites/*.caddy`(额外站点扩展点,见 README 同名章节),Caddy 仍停在 **2.11.4**(cloudsmith 402 挡住了二进制升级,见 §7.6)。OPNsense 的 ACME 自动化里新增两条指向本机的(2026-10-11 定型):`send to oraclejp-caddy`(SFTP,user `ubuntu`,**直传** `/etc/caddy/certs/netbird`)+ `reload caddy oraclejp`(SSH,命令只有 `sudo systemctl reload caddy`),host 写 IPv4 `140.83.87.53`;交付契约与三个坑见 §7.5。
- **euserv**:除了是本仓库 WARP 脚本的典型目标(IPv6-only 需要 v4 出口),也是用户「真实部署」的节点之一,客户端 passwall 曾用 `euserv-cf.henhaoji.site`。
- **额外站点(所有脚本管理的 VPS,2026-10-10 起)**:主 Caddyfile 由脚本整体重写,因此**手工加的站点块必丢**。现在统一改为放进 `CADDY_SITES_DIR`(默认 `/etc/caddy/sites`):脚本只在 Caddyfile 末尾渲染一行 `import /etc/caddy/sites/*.caddy`,目录内容归用户所有(只建目录;目录全空时补纯注释的 `00-example.caddy`),install/upgrade/uninstall 都不动用户文件。硬性要求两条:**站点块必须 `bind 127.0.0.1 [::1]`**(否则落到未包装的 `:8003` 监听器,443 回落流量到不了它)、**通配符必须自己喂证书**(apt 版 caddy 无 DNS 插件,签不了通配符;换证书后要 `systemctl reload caddy`,因为 caddy 不 watch 证书文件)。细节见 `README.md`「额外站点」一节。
---
## 3. cdn-bypass 架构(两层,gcoregon 已上线)
数据流(**本机原生出站**):
```
本机出站包
→ nft mangle OUTPUT:目的地址 ∈ 188 官方 CDN 段 且 th dport ∈ {53,80,443} → meta mark set 1
→ ip rule fwmark 1 lookup 100 → table 100: local default dev lo (纯内核运行时状态)
→ nft tproxy prerouting:mark 1 → TPROXY 到 127.0.0.1:54443
→ xray dokodemo-door(54443) ──routing inboundTag=cdn-bypass-in──→ cdn-bypass-out(VLESS 上游, sockopt.mark=255)
→ nft 首条规则豁免 mark 255(防环:上游 IP 自己也落在 CDN 段内时)
```
关键设计(用户拍板):
1. **所有筛选都在 nft 层**(CIDR + 端口 `th dport`),xray 片段是零筛选的 catch-all dokodemo→proxy。
2. **只有 proxy 出站带 mark 255**;**主配置的 direct 出站绝不能带 mark 255** —— 否则「客户端经本机访问 CF」的流量会被豁免、直连出去,在 GCP 上表现为 **「本机 curl 正常、客户端不通」**。这是 2026-09-27 那个「客户端连不上 cloudflare.com」的根因。
3. **ip rule / ip route 不会被任何东西持久化**,故由 oneshot `cdn-bypass-route.service` 负责恢复。
4. **drop-in 必须叫 `20-cdn-bypass-confdir.conf`**:官方 xray 安装脚本自带 `10-donot_touch_single_conf.conf`(把 ExecStart 重置回单 `-config` 形式),同前缀下 `c` < `d` 会**后**加载并静默盖掉 `-confdir`。
5. **CDN 段只收官方来源**:CF `ips-v4`(15) + Fastly `public-ip-list`(19) + Akamai/Linode WHOIS(154) = **188 条**(2026-09-28 审计)。早期 401 条里有 213 条其实是 Let's Encrypt 验证源 IP 被误收(NTT/Level3/Verizon/GTT/IBM Cloud… ),已剔除;测试断言锁在 `[180,200]` 防止再膨胀。用户明确要求:**只按官方公布的段转发,不要自己发明**。
---
## 4. ECH 联调结论(客户端 passwall / Xray)
- Xray ECH **动态串**里的 DoH 必须是 **`/dns-query`**(RFC 8484 wireformat,POST + h2)。用户最初填 `/resolve`(那是 JSON 调试接口)→ 查询超时。
- Xray 的 ECH 查询只支持 `udp://` / `https://` / `h2c://` 三个分支,**没有 `tcp://`**。
- 用户实测**采用**:`域名+udp://9.9.9.9`(明文 UDP53)。理由:`8.8.8.8` UDP 偶发被断、其 443 DoH 不稳(疑似被 passwall 本机代理劫入代理通道);Quad9 延迟略高但不被屏蔽。脚本默认值即此(commit `9ca581c`)。
- 客户端所在墙内网络:type65/HTTPS 普通 DNS 查询被过滤(`dig` 走 `127.0.0.53` 和 `@1.1.1.1` 均失败);`cloudflare-dns.com` 与 `1.1.1.1:443` 被封;`dns.google`、`dns.alidns.com` 可达。
- CF 轮换 ECH 密钥很勤,**静态 base64 快照很快失效** → 脚本默认用动态 `ech=`(三态:默认动态 / `off` / 原样自定义)。
---
## 5. Northflank(兜底节点)
- team **`stayawakes-team`**(不是 `stayawakes`,后者 403)、project `scilad`、service `erepr-docker-for-ladder`、template `scilad`。
- **`autorun: false`**:改 template **不会**影响运行中的服务,需要手动 run/sync。
- 配置以 template 的 **runtimeFiles base64 内嵌**(`/usr/local/etc/xray/config.json` + `/etc/hysteria.yaml`),**不是** secret group。
- 已完成的优化:删掉 private 443 上的死 REALITY inbound 与 p01 端口;XHTTP :8443 加 `xPadding` extra(与分享链接的 `extra=` 逐字节一致);修 routing 里指向不存在出站的 `outboundTag: "block"` → `"blocked"`;healthChecks 增加 TCP livenessProbe :8443(template 与 live service 都写了)。
- hy2 配置原样保留,但**UDP 公网不可达**(需要 UDP L4 LB 才能用)。
- **教训**:Northflank token 是 JWT,**绝不能 `source .env` 加载**(含 shell 特殊字符会被吃空 → 401),必须 `grep + cut` 原样取;Token ID(`dockerno1-vpsetup`)只是轮换管理句柄,认证只用整串 JWT 作 Bearer。egress 计费 $0.06/GB。
---
## 6. 凭据在哪(都不在仓库里)
| 位置 | 内容 |
|---|---|
| 各 VPS `/etc/caddy-xray-hy2/setup.env`(mode 600) | DOMAIN / EMAIL / WS_PATH / XRAY_UUID / 私钥 / shortId / CF_API_TOKEN / hy2 密码 |
| 本仓库 `.env`(gitignored,永不提交) | `northflank_api_token`、`northflank_xhttp_link`(含真实 uuid 与 xPadding) |
| `~/.dsh/archive/vpsetup-claude-2026-09-28/` | 旧会话原始记录(含明文凭据,仅本机留档) |
| 各 VPS `/etc/caddy-xray-hy2/migration-backup-<ts>/` | 迁移前的旧 Caddyfile / xray config / hysteria config |
---
## 7. 未决项 / 待办
1. **⚠️ EUserv VPS 当前不可达(2026-10-09 实测)**。从本机(无 IPv6 路由)与从 dockerno1(IPv6 出网正常,`ping6 2606:4700:4700::1111` 通)**双双** 100% 丢包 + SSH 超时;橙云域名 `euserv-cf.henhaoji.site` 也超时。
- 注意与 **2026-09-26 那次整机宕机**区分:那次是 EUserv 侧排队执行电源操作,用户等到恢复;本次现象类似但尚未确认。
- 排查方向:① EUserv 面板看机器状态;② `2a02:180:6:1::7ca` 这条 AAAA 是否因重装/换网卡而失效(地址后缀像 MAC 派生,重装可能变);③ 可用任一节点 `setup.env` 里的 `CF_API_TOKEN` **只读**查 Cloudflare DNS 记录核对当前地址。
2. ~~**`gcorhy.henhaoji.site`**~~ —— **已了结(用户 2026-10-10:该域名本身就废弃了,不用管)**。它原是旧 hysteria 测试残留的 Caddy 站点块(`reverse_proxy / → 127.0.0.1:8998`,后端早已不存在),迁移时原样抽到 `/etc/caddy-xray-hy2/migration/caddy-extra-sites.conf`;**不要**并进新 Caddyfile,CF 上那条 DNS 记录留着也无所谓。
> ℹ️ `migrate_existing_stack.sh` 里「额外站点块如何抽取/合并」的逻辑**已改造完成**(2026-10-10):现在直接安装到额外站点目录,见第 4 条。
3. 后续新增主机时,记得同步更新本文件与 `AGENTS.md` 的访问地图。
4. ~~**迁移脚本接入站点扩展点**~~ —— **已完成(2026-10-10)**:`migrate_existing_stack.sh` 抽出的非主域站点块现在**直接安装**到 `CADDY_SITES_DIR`(默认 `/etc/caddy/sites`;干跑落 `DRY_RUN_DIR/sites/`)的 `migrated-extra-sites.caddy`,升级后由 `import` 自动加载,不再提示人工合并;参考件仍留一份在迁移输出目录。两道保护刻意保留:含 `dns <provider>` 定制插件指令、或(装了 caddy 时)`caddy adapt` 预检不通过 → **只留参考文件**,因为装进去会让 `caddy validate` 失败、反而挡住升级。另外缺 `bind 127.0.0.1` 的块会告警,目标文件已存在时先备份 `.bak-<时间戳>`。
5. **netbird 公网入口(oraclejp,2026-10-11 定型)**:VPS 侧承担两块能力——① 管理面:`netbird.savorcare.com` → Caddy 站点块 → 公司 `qhxxg.savorcare.com:10443`(客户端不必再记端口);② 访客面:`*.nb-pub.savorcare.com` → Caddy → 本机 netbird proxy `https://127.0.0.1:8444`(`tls_insecure_skip_verify`)。
- ✅ **VPS 侧扩展点已上线并实测**:`/etc/caddy/Caddyfile` 末尾有 `import /etc/caddy/sites/*.caddy`(第 101 行)、`/etc/caddy/sites/00-example.caddy` 就位。跑过一次完整 `ACTION=upgrade` 验证:升级前放进去的用户站点块在重渲染后**原样保留且继续被服务**(SNI 与 HTTP 均通),xray/hysteria 配置与凭据指纹不变(客户端链接不受影响)。该次升级同时验证了新的 apt 容错(见第 6 条)。
- ✅ **DNS**:用户已把 `*.netbird.savorcare.com` 改为 CNAME → `oraclejp.henhaoji.site`(=140.83.87.53)。注意 savorcare.com 的 NS 在 **Namecheap**(不是 Cloudflare),且 apex 通配 `*` A 记录仍指公司固定 IP。
- ✅ **OPNsense 自动化(最终形态,2026-10-11 定型)**:
* `send to oraclejp-caddy`(SFTP / `configd_upload_sftp`):user `ubuntu`,**直传最终目录 `/etc/caddy/certs/netbird`**,文件名 `fullchain.pem`/`key.pem`/`cert.pem`/`ca.pem`,`sftp_chmod=0644`、`sftp_chmod_key=0640`、`sftp_chgrp` 留空。
* `reload caddy oraclejp`(`configd_remote_ssh`):user `ubuntu`,命令**只有** `sudo systemctl reload caddy`(不再有 install/chown/chmod 步骤)。
* 两条 host 都写 **IPv4 字面量 140.83.87.53**(原因见下面的坑 3)。
- ✅ **交付契约(最终形态)**:目录 `/etc/caddy/certs/netbird` = **`ubuntu:caddy 2770`**(setgid ⇒ SFTP 新传的文件自动继承 `caddy` 组,所以自动化里不需要任何 chown/chgrp/install);文件 `fullchain.pem`/`cert.pem`/`ca.pem` = 644、`key.pem` = 640,owner = SFTP 用户 `ubuntu`。
* Caddy(uid 996 / group 996)走 **group caddy** 读 ✓
* netbird proxy 容器(镜像固定 `netbird:netbird` = uid/gid 1000)走 compose 里的 **`group_add: ["996"]`** 读同一个组 ✓
* 其他账号读不到私钥 ✓(`opc` 1000、`ubuntu` 以外)
实跑验证:重跑自动化后 4 个文件直落该目录、组=caddy;用 `setpriv --reuid=1000 --regid=1000 --groups=996 -- test -r`(正确模拟容器身份)确认可读;Caddy `reload` 正常。
ℹ️ 中转目录 `/home/ubuntu/caddy-certs-incoming` 已删除(不再需要)。
- ⚠️ **坑 1:`sftp_filename_cert`/`sftp_filename_ca` 留空 ≠ 不上传**,而是回落到插件默认模板 `{{name}}/cert.pem`、`{{name}}/ca.pem`(`{{name}}` = 证书名)→ 目标目录里会多出一个以证书名命名的子目录。要么显式写文件名(现在这样),要么接受子目录。
- ⚠️ **坑 2:站点块域名必须被所配证书覆盖**。证书重签后 SAN 从 `*.netbird.savorcare.com` 变为 `*.nb-pub.savorcare.com`,旧站点块仍在服务旧名字 → Caddy 判定证书不匹配,**自行发起 ACME 申请**(泛域名只能 DNS-01,官方 apt caddy 无插件 → 必然失败),3 分钟重试 23 次、并会撞 CA 速率限制、把负载拉到 4+。处置:旧块改名 `netbird.caddy.disabled`、新增 `nb-pub.caddy` 绑定 `*.nb-pub.savorcare.com`,reload 后重试降为 0(实测)。
- ⚠️ **坑 3(可复用到别的主机)**:ACME 插件生成的 SSH 身份限制 `from="<它按 IPv4 算出的源地址>"`,目标若是双栈域名,sshd 可能**经 IPv6 连入** → 日志报 `Authentication tried … with correct key but not from a permitted host`。解法:automation 的 host 写目标 IPv4 字面量。
- ✅ **站点块现状**:
* `netbird-mgmt.caddy`:`netbird.savorcare.com` → `https://qhxxg.savorcare.com:10443`(`header_up Host` + `tls_server_name` + `versions 2`,实测 HTTP/2 200,管理面走这里)。
* `nb-pub.caddy`:`*.nb-pub.savorcare.com` → `https://127.0.0.1:8444`(`tls_insecure_skip_verify`),SNI 实测出示 `CN=netbird.savorcare.com` ✓。
* `netbird.caddy.disabled`:旧访客块,停用保留以便回滚(要用回 `*.netbird.savorcare.com` 必须先把该名字重新加进证书 SAN,否则又会触发坑 2)。
* ⏳ **待办**:`*.nb-pub.savorcare.com` 的 DNS 目前仍指向公司 IP(183.238.250.166),需要指到 oraclejp(140.83.87.53)访客入口才真正可用。
- ✅ **证书现状**:`netbird` 证书 `statusCode=200`,主体 `CN=netbird.savorcare.com`,SAN = **`*.nb-pub.savorcare.com` + `netbird.savorcare.com`**,到期 **2027-01-09**(2026-10-11 重签)。
⚠️ 那批"失败"提示多来自插件已知 bug:`GET /certificates/automation/<uuid>` 即使成功也返回 `{"result":"failed"}`,别拿返回体判断成败;判断依据看目标机文件与 Caddy 日志。
- 依赖:netbird 公网 proxy 容器已部署(`netbird-public-proxy`,`127.0.0.1:8444`);访客路径当前返回 404 是因为管理面映射尚未完全就绪 / 子域 DNS 未切换。
6. **⚠️ Caddy 官方 apt 源 402(2026-10-09 起,影响全机队)**:cloudsmith 给 caddy 项目的免费包投递配额被 CI 滥用打满,`/public/caddy/stable/deb/debian` 下所有下载一律 402 Payment Required(上游 [caddyserver/dist#142](https://github.com/caddyserver/dist/issues/142),维护者称在等 Cloudsmith 提额;2023、2024 各发生过一次,2024 那次约 8 天)。**影响**:`apt-get update` 必失败 → 在此之前 `ACTION=upgrade` 在第一步就中止(与本次站点扩展点改动无关)。
- 已按用户决定实现**窄容错** `apt_update_tolerant()`:仅当报错**全部**来自 caddy/cloudsmith 源**且**本机已装 caddy → WARN 后继续(配置照常渲染 / validate / reload);报错牵涉其他仓库、或本机没有 caddy → 依旧硬失败。oraclejp 实测:`ACTION=upgrade` 现在能跑完,caddy 停 2.11.4、Xray 停 26.3.27。
- 若长期不恢复,可改用 Caddy 官方 GitHub Release 的 `.deb`(同一份产物,附带 Sigstore 签名与校验和);但那是供应链口径变更,需用户拍板,脚本默认不做。
---
## 8. 常用运维命令
```bash
# 巡检一台(把 <user>@<host> 换掉即可)
ssh <user>@<host> 'systemctl is-active caddy xray hysteria-server; \
sudo ss -lntup | grep -E ":(443|8443|54442|8080|54443)"; \
sudo nft list tables; \
curl -s --max-time 8 https://www.cloudflare.com/cdn-cgi/trace | grep -E "^(ip|loc|warp)="'
# 打客户端分享链接(Oracle 两机脚本常驻)
ssh ubuntu@oraclejp.henhaoji.site 'sudo ACTION=links bash /root/setup_caddy_xray_hysteria.sh'
# 升级到最新脚本(幂等,不提示)
ssh <user>@<host> 'sudo ACTION=upgrade bash /root/setup_caddy_xray_hysteria.sh'
# 迁移手工栈 → 脚本管理(先 DRY_RUN 看生成物)
DRY_RUN=1 bash migrate_existing_stack.sh # 本地只写 ./dry-run-output/
sudo bash migrate_existing_stack.sh # 目标机上实跑
# 本地验证(DRY_RUN 断言,不碰系统)
bash -n setup_*.sh migrate_existing_stack.sh && shellcheck setup_*.sh migrate_existing_stack.sh && bash tests/run_tests.sh
```
**判断 gcoregon 的 cdn-bypass 是否在正常工作**:`curl https://www.cloudflare.com/cdn-cgi/trace` 的 `ip=` 应为 **alius 的 43.110.45.237**(而不是 GCP 自己的出口);非 CDN 目标(如 AWS checkip)应仍是 GCP 出口。
---
## 9. OPNsense ACME 自动化:可复用配方(2026-10-10 实操记录)
API 基址 `https://192.168.254.1/api/acmeclient`(只能从内网调,例如 dockerno1;基本认证,凭据在 `~/cc-499/.env` 的 `opn_key`/`opn_secret`)。
| 目的 | 调用 |
|---|---|
| 读全部配置 | `GET /settings/get`(嵌套结构:`certificates.certificate.<uuid>`、`actions.action.<uuid>`、`validations.validation.<uuid>`、`accounts.account.<uuid>`) |
| 新建自动化 | `POST /actions/add`,体 `{"action":{…}}` → `{"result":"saved","uuid":"…"}` |
| 改自动化 | `POST /actions/update/<uuid>`(体同上,字段级合并) |
| 新建证书 | `POST /certificates/add`,体 `{"certificate":{…}}` |
| 改证书 / 挂自动化 | `POST /certificates/update/<uuid>`;`restartActions` 是**逗号分隔的 uuid 串**(不是数组) |
| 立刻签发某证书 | `POST /certificates/sign/<uuid>` —— **异步**(`daemon -f`),接口立即返回 `{"response":"OK"}`,结果要看证书的 `statusCode`/`lastUpdate` |
| 重跑某证书的自动化 | `GET /certificates/automation/<uuid>` —— **成功也返回 `{"result":"failed"}`**(插件已知 bug,别看返回体,看目标机文件) |
| 取身份公钥 | `POST /actions/sshGetIdentity`(`remote_ssh_identity_type=ecdsa&remote_ssh_host=…`)或 `POST /actions/sftpGetIdentity`(`sftp_identity_type=rsa&sftp_host=…`)→ `{"status":"ok","identity":"…"}`;**必须 POST**,GET 只会得到 `{"status":"unavailable"}` |
| 连接测试 | `POST /actions/sshTestConnection` / `POST /actions/sftpTestConnection`(字段同对应 automation)→ `{"actions":[…],"success":true}` |
要点与坑:
- 身份**按类型共享**(`rsa` 给 SFTP、`ecdsa` 给 remote SSH,实际存 `/var/etc/acme-client/sftp-config/id.<type>`),公钥行自带 `restrict`(SFTP 另加 `command="internal-sftp"`)与 `from=` 限制,**整行**贴进目标机 `authorized_keys` 即可。
- 证书 `name` = CN / 主标识符,**必须是域名**(填标签会 400,见 §7.5 坑 1);`altNames` 才是附加 SAN,逗号分隔。
- 自动化与证书记录直接写 config.xml,**不需要** `service/reconfigure`;只有改 settings(启用、challenge 端口等)才需要。
- 插件 cron 每天 00:00 跑 `acmeclient cron-auto-renew`;`restartActions` 在**签发/续期成功后**按顺序执行。
- ⚠️ **凭据纪律(本会话踩过)**:`settings/get` 的返回里明文含 Namecheap API key、ACME 账户私钥等;解析打印时必须按字段名脱敏,但**只匹配 key/user/pass/token 是不够的**——`dns_namecheap_api` 就是这样漏出去的。宁可白名单(只打印已知非敏感字段)。
---
## 10. REALITY 回落 / 按域名分流:源码级结论(2026-10-10)
排查"能不能让特定域名先转发到别处、其余兜底 Caddy"时做的核对(Xray-core v26.10.10 源码 + xtls.github.io 文档 + oraclejp 实机验证)。**三层不要混**:
| 层 | 字段 | 何时触发 | 能否按域名 |
|---|---|---|---|
| REALITY | `realitySettings.dest` / `target`(+`xver`) | 外层握手**不是**合法 REALITY 请求时(浏览器/扫描器/任意 SNI) | **不能**:`dest` 在读到 ClientHello 之前就被 dial,单一无条件目标 |
| VLESS | `settings.fallbacks[].dest`(+`name`/`alpn`/`path`/`xver`) | 外层握手**成功**且 VLESS 头解析失败(UUID 不对、非 VLESS、明文 HTTP) | **能**,见下 |
| 自建 | `dest` → 本机 SNI 预读路由器(HAProxy `req_ssl_sni` / nginx `ssl_preread`) | 上述"非 REALITY"流量先过路由器 | **能**,且是官方文档推荐的思路 |
- `serverNames` **不是路由表**,只是 REALITY 认证白名单:SNI 不在名单里 → 判为非 REALITY → 同样进 `dest`。实机验证:SNI=`netbird.savorcare.com`(不在名单)照样到 Caddy 并出示 netbird 证书;未知/无 SNI 由 Caddy 回 TLS alert 80。
- 线上 oraclejp 的 `settings.fallbacks` **是空的**,所以现在所有回落都由 Caddy 按 SNI 选站点 —— 这就是"按域名分流"在跑的形态(官方 reality.html 也建议"在前面放 Nginx 之类过滤 SNI")。
- `fallbacks` 匹配细则(`proxy/vless/inbound/inbound.go`):先 `name`(**精确**,未命中则**最长子串**匹配——所以写 `netbird.savorcare.com` 能覆盖 `foo.netbird.savorcare.com`,但**不支持通配符**)→ 再 `alpn`(精确→默认项)→ 最后 `path`(只对 HTTP/1.1 首包有意义,≤64 字节探测,**看不到 Host**,域名只能靠 `name`)。相同 alpn+path 的多个子项以**最后一个**为准。
- 版本坑(都会踩):
* **v25.7.26 起**:只写端口的 `dest`(如 `"8003"`)被补成 `localhost`,可能先解析到 `::1`。本仓库 2026-10-10 已改成显式 `"127.0.0.1:8003"`(此前能用只因 Caddy 同时听 `127.0.0.1:8003` 和 `[::1]:8003`;换 IPv4-only 后端就静默失联)。
* **2026-09-08 起**(~v26.9.9):REALITY 拒绝不带 `X25519MLKEM768` 的 TLS1.3 ClientHello → 老客户端**静默**降级为"伪装流量"(表现为连不上但不报错,流量被转发到 `dest`)。
* `fallbacks` 与 VLESS Encryption(`decryption` ≠ none)是**硬配置错误**。
* REALITY 下 `NextProtos` 为 nil → `fallbacks.alpn` 匹配的是**目标站**协商出的 ALPN,不是我们 `tlsSettings.alpn` 的。
* 一个 443 只能"伪装"一个目标站;要多前台域名就得多监听,或用 SNI 路由器。
* PROXY protocol 在同一端口上**全有或全无**;用 SNI 路由器时把 Xray 侧 `xver` 设为 0、由路由器 emit PROXY,避免两个头。
---
## 11. Caddy 会不会掐断 gRPC?——实测结论(2026-10-11,oraclejp)
起因:观察到"经 Caddy 的 gRPC 连接约 0.2 秒就断,直连公司则 300 秒才被 traefik 断",怀疑 Caddy→traefik 参数有问题,并考虑把 `realitySettings.dest` 改成 SNI 路由器做裸 TLS 透传。**结论:两个现象都不是 Caddy 造成的,透传方案不必要(已记录、未实施)。**
**方法**:给 `netbird-mgmt.caddy` 加了一行 `log`(默认写 journal,含 duration/status/error/host),再把 Caddy 访问日志、proxy 容器日志、容器网络命名空间连接表按 UTC 时间对齐。诊断完成后该 `log` 行可留作审计(删掉即恢复无访问日志)。
**证据 1:长流被 Caddy 完整转发,超时来自上游**
```
01:47:01 dur=131.263s status=0 /management.ManagementService/Job host=netbird.savorcare.com
01:47:03 dur=300.074s status=504 /management.ManagementService/Job error=None
resp_headers: Content-Length: 15, Date: …, Via: 2.0 Caddy
```
`Job` 流经 Caddy 存活 **300.074 秒**;504 带 `error: None` + 上游响应头 ⇒ 是**上游 traefik 自己返回的 504**,Caddy 原样透传(Caddy 出超时会记 error 字段)。这与"直连公司 300 秒被 traefik 断"是同一个 300 秒 ⇒ **Caddy 没有缩短任何东西**。
**证据 2:0.2 秒那类条目是 netbird 自己的短流**,例如 `SignalExchange/Send` 的 0.075–2.6 s、`ConnectStream` 0.000 s,全部 **status=200**;signal 客户端本就频繁重建(proxy 日志写明 "will retry silently")。
**证据 3:当时的 1 秒重试风暴也不经过 Caddy**:proxy 以约 1 次/秒报
`mapping stream: … code = Unimplemented desc = unexpected HTTP status code received`(grpc-go 把 HTTP **404** 映射成 Unimplemented)。同期 Caddy 访问日志里该站点**一条 404 都没有**(5 分钟 18 条,全是 200/0/504),而容器连接表采样显示全部指向 `183.238.250.166:**10443**`、到本机 443 的连接数为 **0**。
**证据 4:404 的机制(同一方法 `/management.ProxyService/SyncMappings` 打公司 10443)**
| 请求写法 | 结果 |
|---|---|
| 域名(SNI + Host = `netbird.savorcare.com`) | **200** |
| **直接用 IP**(无 SNI,Host=IP:10443) | **404 `404 page not found`**(traefik 未匹配路由 → Go 默认 404) |
| 用 IP 但补 `Host: netbird.savorcare.com` | **200** |
⇒ proxy 用 **IP:10443** 直连公司,缺 SNI/Host,traefik 无法路由 → 404 → gRPC Unimplemented → 1 秒重试。**这是 netbird 侧端点配置问题,与 VPS/Caddy 无关**(proxy 的 env 是 `NB_PROXY_MANAGEMENT_ADDRESS=https://netbird.savorcare.com`,所以那个 IP 端点来自服务端下发/配置档,不是我们机上的东西)。
**待办(用户侧)**:① 让 netbird 管理面/配置档里的端点用域名 `https://netbird.savorcare.com`(443,经 VPS→Caddy→公司,实测 200),别用 `IP:10443`;② proxy 仍报 `static certificate (SANs [*.nb-pub.savorcare.com netbird.savorcare.com]) does not cover domain "te…"`——有 netbird 服务仍在旧 `*.netbird.savorcare.com` 域下,需与新证书对齐(或把该名字加回 SAN)。
**已记录、未实施的备选(仅当将来证明 Caddy 这一跳确实有害时再做)**:把 `realitySettings.dest` 指向本机 SNI 路由器(HAProxy `req_ssl_sni` / nginx `ssl_preread`),`netbird.savorcare.com` 裸 TLS 透传公司 `10443`、默认回落 Caddy `8003`;注意 REALITY `xver:1` 已插 PROXY 头故路由器需 `accept-proxy`,且需先把 `dest` 参数化(脚本渲染,手改会被 `ACTION=upgrade` 覆盖)。
### 11.1 源码级补充(Caddy v2.11.4 逐行核对,2026-10-11)
- **Caddy 的 reverse_proxy 没有任何 gRPC 专用代码**(`git grep grpc` 只命中 `protocol grpc` 匹配器);`flush_interval -1` 对服务端流式 gRPC 是 **no-op**(`ContentLength == -1` 时本就每次写都 flush)。⇒ 我块里没有需要"为 gRPC 补参数"的地方。
- ⚠️ **`keepalive_interval` 是 TCP keepalive,不是 HTTP/2 PING**:Caddy 从不设置 Go http2 的 `ReadIdleTimeout`/`PingTimeout`(`git grep` 零命中)。所以现在块里的 `keepalive_interval 15s` **并不能让上游 h2 连接保活、也探测不到半开连接**;traefik 若先 GOAWAY 掉池中空闲连接,Caddy 侧只会在下一个请求上撞上"陈旧池连接"。`keepalive <dur>` 才是**池内空闲超时**(默认 2m)。
- **陈旧池连接会怎样**(Go 1.25.1 `h2_bundle.go`):普通已关闭/空闲连接会被透明重试;但 **GOAWAY / RST(REFUSED_STREAM) 打在进行中的 gRPC POST 上会立即失败**(Caddy 从不设置 `GetBody`,无法重放)→ 表现为**极快的 502**;若失败发生在响应头之后则 `panic(http.ErrAbortHandler)` → 客户端看到 **RST_STREAM**。这解释了"502 与 RST_STREAM 交替"=同一个上游竞争,而非两个 bug。**本次实测没有出现这类 502**(该站点日志里只有 200/0/504),所以它不是当前症状的原因。
- ⛔ **不要为了这个去升级 Caddy**:**v2.11.6** 起默认给上游加了 1 分钟 idle 读/写超时,反而**打破 h2/流式**(#8103 / #8118 / #8101);而 **v2.11.4 自身**有一个未修复的挂起 bug **#8185**(PR #7719 引入):上游在**请求体读取途中** RST_STREAM 时,被阻塞的 `Read` 永不返回 → 请求挂住(复现条件正是"上游等约 300ms 后 RST",v2.11.3 不复现)。⇒ 若将来看到**挂住**而不是断开,先怀疑这条。
- `versions 2` **不是"只用 h2"**:Go 的 `http2.ConfigureTransport` 会把 `h2` 与 `http/1.1` 都写进 `NextProtos`,Caddy 的 `versions` 只影响非 TLS(h2c)。⇒ 想确认 Caddy→traefik 这一跳真的是 h2,要看 **traefik 访问日志的 `proto` 字段**;若被降级成 HTTP/1.1,gRPC 会以诡异方式失败而普通 HTTP/2 请求看不出来。
- traefik 默认值(供对照):`readTimeout` 60s、`idleTimeout` **180s**、`http2.maxConcurrentStreams` 250;ServersTransport `idleConnTimeout` 90s、`readIdleTimeout` 0s、`pingTimeout` 15s。实测那个 300s **不等于** 180s 默认值 ⇒ 用户侧有显式配置(未核实)。
- 若将来真出现"经 Caddy 极快 502":可低风险加固 `reverse_proxy` 里加 `keepalive 60s`(在上游 GOAWAY 之前主动丢弃池连接);排查用全局 DEBUG(站点 `log` 只出 INFO/ERROR 访问日志,`http.handlers.reverse_proxy` 的 DEBUG 行才带 `upstream roundtrip` 的错误原因)或 `systemctl edit caddy` 加 `Environment=GODEBUG=http2debug=2`(唯一会打印 GOAWAY/RST_STREAM/重试的地方)。⛔ 别给 gRPC 开 `lb_try_duration`/`lb_retries`(默认只重试 GET,重放已消费的体会造成空体 POST → #8166)。