80 Commits

Author SHA1 Message Date
eli 4502595776 docs(research): 归档 Caddy v2.11.4 上游 gRPC/h2 长流源码级调研报告(含引用与逐行证据) 2026-10-11 09:54:22 +08:00
eli 22e9078d79 docs(HANDOFF): 补 Caddy v2.11.4 源码级结论(keepalive_interval 非 h2 PING、别盲升级、2.11.4 有 #8185 挂起 bug) 2026-10-11 09:54:14 +08:00
eli 700e609e01 docs(HANDOFF): 实测证明 Caddy 不掐 gRPC——300s 是 traefik 的 504、404 是 proxy 用 IP 直连公司缺 SNI/Host
给 netbird-mgmt.caddy 临时加 log 后,把 Caddy 访问日志 / proxy 容器日志 / 容器 netns 连接表
按 UTC 对齐,结论:

1) Job 流经 Caddy 存活 300.074s,状态 504 且 error=None、带上游响应头与 Via: 2.0 Caddy
   ⇒ 504 是上游 traefik 发的,Caddy 只是透传(出超时会记 error),与直连同为一个 300s;
2) 0.2s 那类条目是 netbird 自身的短流(SignalExchange/Send 0.075-2.6s、ConnectStream 0s),
   全部 200,signal 客户端本就频繁重建;
3) 期间 1 次/秒的重试风暴(Unimplemented = HTTP 404)不经 Caddy:Caddy 该站点 5 分钟
   18 条日志无任何 404,容器连接表全部指向 183.238.250.166:10443、到本机 443 为 0;
4) 同一方法 SyncMappings 打公司 10443:用域名 200、直接用 IP 404(Go 默认 404 页)、
   用 IP 加 Host 头又是 200 ⇒ 根因是 proxy 以 IP 直连公司、缺 SNI/Host 导致 traefik 未匹配。

因此 dest+SNI 裸透传方案不必要(已记录未实施),并把 netbird 侧两项待办写清。
2026-10-11 09:52:18 +08:00
eli cb631c83f1 docs(HANDOFF): netbird 证书交付定型为「SFTP 直传最终目录 + SSH 只 reload」
把之前来回改过的临时方案(incoming 中转目录、install -o root -g caddy / -o caddy -g 1000、
SSH 里做 chown/chmod)全部作废,记录最终形态与实测结果:

交付契约:/etc/caddy/certs/netbird = ubuntu:caddy 2770(setgid ⇒ 新文件自动继承 caddy 组),
fullchain/cert/ca.pem 644、key.pem 640;Caddy 走 group caddy 读,netbird proxy 容器
(镜像固定 uid/gid 1000)走 compose 的 group_add:[996] 读同一个组;其他账号读不到私钥。
自动化里不再有任何 install/chown/chmod —— SFTP 直传即可,SSH 只 systemctl reload caddy。

并记录三个真实踩到的坑:
1) sftp_filename_cert/ca 留空 ≠ 不上传,会回落到插件默认模板 {{name}}/cert.pem → 多出子目录;
2) 站点块域名必须被所配证书覆盖,否则 Caddy 自行发起 ACME 申请(泛域名只能 DNS-01,
   必然失败)——本次证书 SAN 改成 *.nb-pub.savorcare.com 后旧块触发 3 分钟 23 次重试、
   负载 4+,处置后降为 0;
3) 双栈目标下 ACME 身份的 from= 只按 IPv4 算,automation host 必须写 IPv4 字面量。
另记:GET /certificates/automation/<uuid> 成功也返回 {"result":"failed"}(插件已知 bug)。
2026-10-11 09:28:42 +08:00
eli a9cd46906c fix(xray): REALITY dest 写全 127.0.0.1:8003(裸端口在 v25.7.26+ 会变 localhost)
源码核对(Xray-core v26.10.10 + xtls/reality 8cdf7bf):v25.7.26 起只写端口的 dest
会被补成 localhost,可能先解析到 ::1。本机 Caddy 恰好同时监听 127.0.0.1:8003 与
[::1]:8003,所以一直没暴露;一旦回落后端只监听 IPv4 就会静默失联(配置合法、服务照跑、
但回落流量到不了)。改成显式 127.0.0.1:8003,并在渲染函数头注释里写明原因防止被改回去。

顺带把这一轮核到的 REALITY 回落/分流语义写进 HANDOFF §10:REALITY 层 dest 是单一
无条件目标(serverNames 只是认证白名单,不是路由表);按域名分流只能在
① 终结 TLS 的后端(我们现在就是 Caddy)或 ② dest 指向本机 SNI 预读路由器;
VLESS fallbacks 仅对「外层握手成功但 VLESS 头非法」的流量生效,name 支持最长子串匹配
(无通配符)。另记三个版本坑:MLKEM768 要求导致老客户端静默降级、fallbacks 与
VLESS Encryption 互斥、REALITY 下 fallbacks.alpn 匹配的是目标站的 ALPN。
2026-10-10 22:11:43 +08:00
eli 1736a8a0d4 docs(HANDOFF): netbird 证书交付契约——同时满足 Caddy 与 netbird-proxy 的 UID/GID
netbird 的 reverse-proxy 镜像以 netbird:netbird(UID/GID 1000) 运行且 distroless;
本机 ubuntu=1001 / opc=1000 / caddy=996。原来的 root:caddy 640 key 容器读不了
(实测 sudo -u '#1000' test -r 失败),容器一起来就会加载证书失败。

改成双身份方案:install -o caddy -g 1000 -m 644(fullchain)/640(key)
  * Caddy 走 owner(caddy 996)  ✓
  * 容器走 group(1000)        ✓
  * 其他普通用户(含 ubuntu)读不到私钥 ✓
属主/属组写进 OPNsense 自动化命令,换证时重新落成该状态(已重跑自动化实测验证)。

同时记录容器侧挂载与 env(/etc/caddy/certs/netbird:/wildcard-certs:ro、
NB_PROXY_CERTIFICATE_DIRECTORY/FILE/KEY_FILE,不要设 NB_PROXY_WILDCARD_CERT_DIR)
与取舍:group 1000 = 本机 opc 组,opc 账号能读私钥;更紧的替代是容器加
group_add: ["996"] 后把属组改回 caddy。
2026-10-10 16:48:15 +08:00
eli 7550a65c61 feat(migrate): 额外站点块直接装进站点目录——迁移后不再需要人工合并
配合 setup_caddy_xray_hysteria.sh 的 /etc/caddy/sites 扩展点:迁移抽出的非主域站点块
现在直接安装为 CADDY_SITES_DIR/migrated-extra-sites.caddy(干跑落 DRY_RUN_DIR/sites/),
升级后由新 Caddyfile 末尾的 import 自动加载,install/upgrade 永不覆盖该目录;
参考件仍留一份在迁移输出目录。

刻意保留两道「只留参考文件、不自动安装」的保护(装进去会让 caddy validate 失败、
反而挡住升级):① 块里含 dns <provider> 等定制插件指令(官方 apt caddy 不含该插件);
② 装了 caddy 时 `caddy adapt` 预检不通过(只解析适配,不做 TLS 签发)。
另外:缺 bind 127.0.0.1 的块会告警(443 回落流量到不了它)、目标文件已存在时先备份
.bak-<时间戳>、结束语按是否真装分别提示。

测试 +6(共 194):dns 插件块不安装、干净块安装且原文保留、重复迁移先备份、缺 bind 告警。
文档:README 迁移细则 + AGENTS 脚本 4 描述 + HANDOFF §7.2/§7.4。
2026-10-10 11:05:37 +08:00
eli 13637a1c9e docs(HANDOFF): netbird 落地结果 + OPNsense ACME 自动化可复用配方
- §7.5:证书已签发(CN=netbird.savorcare.com,SAN=*.netbird.savorcare.com,
  到期 2027-01-08),两条自动化跑通;站点块 /etc/caddy/sites/netbird.caddy 上线,
  公网 SNI 实测出示该证书、HTTP 502(等 netbird proxy 容器)
- 记录两个坑:① 证书 name 字段必须是域名(填标签会导致 lego 下单 400,且挑战 TXT
  根本不写,很像 DNS 问题)——本次真正的根因;② 插件身份公钥的 from= 只按 IPv4 计算,
  双栈目标会被 IPv6 连入拒绝,automation host 要写 IPv4 字面量
- 记录内网 split-horizon:公司内解析 *.netbird.* → docker1(192.168.254.51),
  与 VPS 的 502 表现不同,别误判
- 新增 §9:OPNsense ACME API 可复用配方(端点/字段/异步签发/已知返回体 bug)
  与凭据脱敏教训(dns_namecheap_api 曾被漏打印)
2026-10-10 10:53:53 +08:00
eli 084bba7226 fix(apt): Caddy 官方源 402 的窄容错——upgrade 不再在第一步就中止
背景(上游 issue caddyserver/dist#142):cloudsmith 给 caddy 项目的免费包投递配额被
CI 滥用打满,2026-10-09 起 /public/caddy/stable/deb/debian 下所有下载一律 402
Payment Required;2023/2024 各发生过,2024 那次约 8 天。后果是 apt-get update 必失败,
而脚本把它放在 system_prepare 第一步 + set -e ⇒ ACTION=upgrade 在任何机器上都跑不完
(哪怕只是想重渲染配置)。这与本次站点扩展点无关,是既有的机队级故障。

容错刻意做得窄(apt_update_tolerant):
  * 报错【全部】来自 caddy/cloudsmith 源 且 本机已装 caddy → WARN 后继续
    (配置照常渲染 → caddy validate → 重载服务,只是本次不升级 caddy 二进制)
  * 报错牵涉任何其他仓库,或本机还没有 caddy → 依旧硬失败(不吞真问题)
install_caddy 里 apt-get install caddy 失败也做同样判断。

测试:从脚本抽出该函数,在隔离环境用假 apt-get / 假 caddy 跑三种分支(继续/硬失败/
硬失败)。oraclejp 实跑 ACTION=upgrade 退出码 0:caddy 停 2.11.4、Xray 停 26.3.27,
配置与凭据指纹不变,用户站点块在重渲染后原样保留并被服务。

文档:README「Caddy 官方 apt 源不可用时」小节 + AGENTS 脚本 2 描述 + HANDOFF §7.6
(含 netbird 落地进度与证书 400 卡点)。
2026-10-10 10:48:10 +08:00
eli addd631e79 docs: 额外站点扩展点(README 章节 + 环境变量表 + AGENTS/HANDOFF)
- README:新增「额外站点(升级安全的扩展点)」章节:用法、升级安全语义、
  四条硬性约束(bind / alpn / 通配符必须喂证书 / 换证后 reload)与写错会挡住升级的联动
- AGENTS.md:脚本 2 描述补扩展点与两条约束
- HANDOFF:§2 补统一约定;§7 新增迁移脚本接入扩展点的待办 + netbird 公网入口
  (oraclejp)的定稿:专用 *.netbird.savorcare.com 证书(密钥最小化,不用公司通配符)、
  照搬 docker1/2 的 OPNsense automation(SFTP 上传 + SSH 安装并 reload caddy)
2026-10-10 09:59:26 +08:00
eli d848a14f43 feat(caddy): 额外站点目录扩展点——升级安全的 import,手加站点不再被 upgrade 覆盖
主 Caddyfile 由脚本整体重写,手工加的站点块必丢(迁移脚本也只能提示人工合并)。
新增 CADDY_SITES_DIR(默认 /etc/caddy/sites):脚本只在末尾渲染
`import $CADDY_SITES_DIR/*.caddy`,对该目录只读——只建目录 + 目录全空时补一个
纯注释的 00-example.caddy,永不覆盖/删除用户文件(uninstall 也不动)。

用 glob 而非字面路径是刻意的:Caddy 对空匹配的 glob 只警告不报错(v2.0→v2.11
一直如此),字面路径不存在才是致命错误,所以目录空着不会挡住升级。

体检(仅告警不阻断):站点块没写 bind 127.0.0.1 时提示——本 Caddy 只在
127.0.0.1:8003 收 Xray REALITY 回落流量(PROXY protocol + tls 包装),缺 bind 的块
会落到另一个未包装的 :8003 监听器上,443 流量永远到不了它(静默不可达)。

测试:import 行渲染、示例纯注释、用户文件不被覆盖、目录非空不补示例、缺 bind 告警、
CADDY_SITES_DIR 覆盖 + 落 setup.env + 拒绝相对路径。
2026-10-10 09:59:19 +08:00
eli 796dcd722b docs: gcorhy 未决项了结(用户确认域名本身已废弃);标注 migrate 额外站点合并逻辑由另一会话负责 2026-10-10 09:44:06 +08:00
eli cba988f55c docs: 新增 docs/HANDOFF.md —— 旧 Claude 会话中的部署事实落库
机队清单(2026-10-09 实测)、各机差异、cdn-bypass 两层架构与 mark255 根因、
ECH 联调结论、Northflank 要点、凭据位置、未决项(EUserv 不可达 / gcorhy)、运维命令。
全文脱敏:不含 uuid/私钥/token。
2026-10-09 23:47:54 +08:00
eli 6c1a9f7d9a docs: 项目指引迁移 CLAUDE.md → AGENTS.md(DSH 原生),补访问地图与 standing constraints
- AGENTS.md 为唯一真源:新增「主控状态与访问」(2026-10-09 实测机队表)、
  「Standing constraints」(rules.v6 归属、凭据纪律、改动闸门、nft 不变式、CDN 清单只收官方段)
- 修正 Commands:syntax/lint 补上 setup_cdn_bypass_tproxy.sh
- 修正 Conventions:origin 是私有 git 服务器,不再写「GitHub web UI」
- CLAUDE.md 保留为指向 AGENTS.md 的短指针(Claude Code 兼容)
2026-10-09 23:47:54 +08:00
eli 057a77f8ff docs: cdn-bypass 注释/CLAUDE.md 对齐新架构(客户端流量同样经 nft 筛选;direct 出站带 255 的典型症状;188 官方 CIDR;drop-in 20- 前缀)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 13:10:11 +08:00
eli 23e75dab24 fix: cdn-bypass drop-in 改名 20- 前缀——官方 xray 安装脚本自带 10-donot_touch_single_conf.conf 会在同前缀 ASCII 序后加载盖掉我们的 -confdir;卸载同时清 10-/20- 两种残留
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 13:04:50 +08:00
eli 12189de816 fix: systemd drop-in ExecStart 从新版 {path=...;argv[]=...} 形式正确抽出 argv[]
新版 systemd systemctl show -p ExecStart --value 输出复合形式,
先前直接把整串写进 drop-in ExecStart= 会被 systemd 拒绝
('argv[]=' 不是合法可执行名),xray 用旧 ExecStart 起来没挂 confdir,
dokodemo 永远不起。

drop-in 现在从 argv[]= 提取纯命令行再附加 -confdir。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 12:51:23 +08:00
eli 1c69bd6bb0 fix: migrate 脚本 xray 解析器补上 # 剥注释(gcoregon 配置含 # 注释无法解析)
剥注释状态机只处理 // 和 /* */,但 gcoregon 历史配置混有 # 风格注释
(如 #"sniffing": { ... #}),json.loads 直接报错迁移失败。

加一行状态机分支:遇 # 在非字符串态也跳到行尾。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 11:55:21 +08:00
eli d4e238cd75 refactor: 端口过滤下沉到 nft 层(xray 不再做端口筛选),架构对齐 nft 全筛原则
- nft mangle 打标链加 `th dport { 53, 80, 443 }`:非 web/DNS 端口的 CDN 流量
  根本不进 tproxy,直接直连。省 upstream 流量的同时 QuIC(udp/443) 自动覆盖。
- xray routing 删 port 过滤:交给 nft 预筛,不再有重复筛分层。
- 测试断言同步反向:xray 不应再写 "port": "53,80,443"。

设计原则:get/post 只在一个地方 —— nft 决定哪些包进 tproxy;xray 只决定
「进来的包走哪个 upstream」。两套筛选条件不重叠就不会出现「nft 抓了 xray
不认」或反过来。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 11:50:01 +08:00
eli 68c29d97d1 fix: cdn-bypass 清单只保留三家公司官方段(401→188,剔除 213 条来源不明的 LE 回源 IP)
2026-09 期间治理 LE 证书续期时,把 LE 验证回源节点的 GCP/Linode IP 误当 CDN 段
加进清单;经 Whois+CF/Fastly/Akamai 官方清单逐条审计后:
- CF 15 条(cloudflare.com/ips-v4)
- Fastly 19 条(api.fastly.com/public-ip-list)
- Akamai/Linode 154 条(AS20940/AS16625/AS63949/AS36183)
共 188 条;剔掉 NTT/Level3/CenturyLink/Verizon/GTT/IBM Cloud/Zayo/AAPT/
CAT Telecom/Softbank/HE/Microsoft/Cologix/PCCW/TPG/IDNIC/Reliance/Telstra/
Telefonica/e-Trade/Fortinet 等 213 条非 CDN 段。

测试断言放宽到 [180,200],防止再次膨胀。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 11:34:52 +08:00
eli 44e1531924 test: cdn-bypass 脚本 DRY_RUN 断言(routing 端口过滤 / mark255 / 双 nft 表 / CIDR≥400)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 08:01:56 +08:00
eli 1b5a0f25e3 fix: cdn-bypass 与 gcoregon 实战清单合并(287→401) + routing 端口过滤
- CDN CIDR 快照 stale:2026-09 扩到 400 条时没回灌脚本,补入 114 条缺失段
  (103.x/122.252/128.241/139.177/157.238/165.254 等 Akamai),只多 1 条 /32
- routing 片段 catch-all → network tcp,udp + port 53,80,443:
  nft 已按 CDN 网段预筛,xray 只补 dokodemo 入站的端口层过滤,
  非 web/DNS 端口直接直连以省上游流量(上游常为计费/限流节点)
- 头注释更新:说明两层的分工、客户端入站不在本脚本范围

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 08:00:56 +08:00
eli 7db64aedd9 fix: northflank 模板容器端口 443->8443(对齐实测:public 端口是 8443,443 是 private 上已移除的死 REALITY)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-26 16:47:47 +08:00
eli d3d8f8a3df docs: northflank/ 部署模板(XHTTP 最优配置 + hy2 + healthcheck 指南)
- xray-config.template.json:边缘终结 TLS 形态(security none / listen 0.0.0.0),
  XHTTP mode auto + extra xPadding(与链接 extra= 一致),routing/dns 同 VPS 模板
- hysteria-config.template.yaml:DNS-01 或自签两方案(边缘独占 80/443,HTTP-01 不可用),
  公网可达需 UDP L4 LB
- README:协议可行性结论(REALITY/hy2 需付费 L4 LB,XHTTP 唯一免费路径)、
  客户端链接模板、healthcheck 须写在 template 服务节点 spec(service 级会被覆盖)、
  TCP 探针优于 HTTP(未认证 GET 返回 404)
- 修正 render_xray_config 上方过时注释:extra 两端一致即可用(4e4d0f4 起链接内嵌),
  原注释仍停留在 15d91b2 的结论

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-26 15:48:28 +08:00
eli f9d829934d feat: 迁移脚本 migrate_existing_stack.sh + WS_PATH 参数化 + ACTION=links
- migrate_existing_stack.sh:手工部署的 caddy/xray/hysteria2 栈一键迁入脚本管理
  - 只读解析三份旧配置(python3 注释感知花括号扫描/注释剥离+json/awk YAML)
  - 交互确认 DOMAIN/EMAIL/WS_PATH,凭据脱敏清单确认后落盘
  - 写 setup.env(%q 格式 mode 600,保留旧 uuid/私钥/shortId/密码,同协议旧客户端不断线)
  - 备份旧配置到 migration-backup-<ts>/;非主域站点块 verbatim 抽取提示人工合并
  - nft 只体检不修改:脏表生成参考配置,绝不插入 include
  - REALITY 公钥用 xray x25519 -i 现推;HY2_CERT_MODE 按旧 acme 配置推断
- setup_caddy_xray_hysteria.sh:
  - WS_PATH 参数化(默认 /scilad,迁移可保留 /vlessws 等旧路径,进 RENDER_VARS)
  - 新增 ACTION=links:只读 setup.env 重打全部分享链接,不碰系统
  - 环境变量优先于 setup.env:sudo CDN_DOMAIN=x ACTION=upgrade 带参升级真正生效
- 测试 167 条断言全绿(fixtures 已脱敏);README/CLAUDE.md 同步
- 已在 oraclejp/oraclejp-2 两机实测:迁移+升级+CDN 端到端验证通过

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-26 15:31:41 +08:00
eli ff77b7e69f chore: gitignore 加入 .env(Northflank API token 等本地机密,不入库)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-26 15:31:15 +08:00
eli b4e53c5d27 docs: README 重构——索引、选型总览、mermaid 架构图、新增 cdn-bypass 章节
CLAUDE.md 同步为三脚本并补 nft 运行时 invariant。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-26 12:25:30 +08:00
eli 8cbd977d22 feat: 新增 setup_cdn_bypass_tproxy.sh —— CDN 绕行透明代理
面向 GCP 等封锁/计费 CDN(Cloudflare/Fastly/Akamai)出口的 VPS:本机到
287 段 CDN 网段的流量经 nft 打标 → ip rule → lo → xray dokodemo tproxy →
上游不限流节点。粘贴 vless:// 链接即用;xray 配置经 confdir 注入不碰原
config.json;ip rule/route 由 oneshot service 持久化;出站 mark 255 防回环。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-26 12:25:30 +08:00
eli e932564fd6 fix: nftables 运行时只加载自己的表,不再执行整份 nftables.conf
Oracle/Debian 云镜像启用 iptables-persistent 时,conf 首行 flush ruleset 会
无差别清空主机防火墙(含 SSH 放行)与 Docker NAT 表,而 persistent 只在开机
补回——运行时执行等于裸奔到重启。改为只 nft -f hysteria.nft(先删后加保证
幂等);开机顺序由 systemd 硬依赖链保证(nftables@sysinit → persistent@
multi-user),flush 无害。卸载路径同步改为只删自己的表,不再 reload nftables。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-26 12:25:30 +08:00
eli 9ca581c1e9 fix: ECH 默认 DNS 8.8.8.8 -> 9.9.9.9(Quad9,实测不被屏蔽;8.8.8.8 偶发断)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-25 15:25:23 +08:00
eli 9385f02421 fix: ECH 默认查询改用 udp://8.8.8.8(明文 UDP53,墙内实测最稳)
8.8.8.8:443 DoH 墙内不稳(疑似被 passwall 本机代理劫进代理通道);
源码只有 udp:///https:///h2c:// 三个分支,tcp:// 不存在。
full 策略下 UDP 投毒只会断连不会静默降级,风险可接受。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-25 13:13:23 +08:00
eli e362c3d3fd fix: ECH default DoH -> 8.8.8.8 IP 字面量(实测连通;免 dns.google 域名二次解析)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-25 12:30:54 +08:00
eli e8787278b7 fix: ECH default DoH endpoint cloudflare-dns.com -> dns.google
Xray ECH DoH 是 RFC8484 wireformat(POST application/dns-message 走 h2),
/resolve 是 JSON 调试接口不认。cloudflare-dns.com/1.1.1.1 墙内实测被封,
dns.google:443 可达。备选 alidns / IP 字面量形式见 README 与 ECH_CONFIG。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-25 12:24:43 +08:00
eli 36e9b06976 feat: CDN links carry ech= — dynamic DoH query form by default
静态 ECH 快照随 CF 密钥轮换很快失效(实测),改为默认内嵌
"CDN域名+https://cloudflare-dns.com/dns-query":客户端 Xray ≥25.7.26
拨号时用内置 DoH 自查并按 TTL 刷新,轮换无感。ECH_CONFIG=off 关闭,
=其他值(静态 base64 / 其他 DoH 端点)原样嵌入。仅 CDN 链接携带
(直连 TLS 在 Caddy 终止,无 ECH)。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-25 12:12:30 +08:00
eli f4d11671c1 feat: XHTTP extra padding on by default (XHTTP_PADDING=0 to disable)
升级注意:旧 setup.env 存的 XHTTP_PADDING 为空,upgrade 后会自动开启并
重新生成 header/key——XHTTP 链接变更,忽略 extra= 的旧客户端会 400。
想保持关闭需在 setup.env 里显式写 XHTTP_PADDING=0。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-25 12:12:16 +08:00
eli 4e4d0f489b feat: optional XHTTP extra padding via XHTTP_PADDING=1 (link carries extra=)
Share-link standard §4.3.19 supports extra= (URL-encoded XHttpObject JSON),
and passwall exposes it as XHTTP Extra. Verified end-to-end with real xray
v26.3.27 + caddy: matching client+server extra → 204 through the full TLS
h2 chain. Default off because a client that ignores the extra param gets
400 from a padding-enabled server.

Also correct the previous claim that links can't carry extra.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-25 00:09:31 +08:00
eli 15d91b2b7a fix: drop XHTTP extra/xPadding block — breaks every link-imported client (400)
End-to-end matrix with real xray v26.3.27 + caddy:
- server with extra padding + plain client: 400 (direct AND via caddy)
- server without extra + plain client: 204 (direct AND via caddy, TLS h2)

Padding placement/key names must match on both ends, but share links cannot
carry these params and mainstream clients can't configure them — so a
server-side-only extra block rejects every real client. Removed from the
template along with XPADDING_HEADER/XPADDING_KEY.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 23:55:38 +08:00
eli 1a040cb54e fix: WS route stripped /scilad prefix (handle_path) — xray 404'd every handshake
Root-caused with live probes: WS upgrade to /scilad returned 404 on both
direct and CDN paths. handle_path strips the prefix before proxying, but
xray wsSettings.path=/scilad expects it intact. Replaced with a site-level
@websockets matcher (path + Upgrade headers) + path-preserving reverse_proxy
in the main block; deleted the redundant handle_path /scilad* in the CDN
block (it also shadowed reverse_proxy @wspaths into dead code).

Verified with real caddy + mock upstream: WS handshake gets 101 and the
upstream receives /scilad unstripped.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 23:27:59 +08:00
eli bc1c751e2a feat: fp=chrome on all TLS client links; ws-cdn link test
Go's native TLS ClientHello fingerprint is a cheap high-confidence proxy
signal (JA3/JA4); make the uTLS fingerprint explicit on ws/xhttp links
(direct + CDN) instead of relying on client-side defaults. Also assert
the ws-cdn link and fp=chrome coverage in tests.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 22:47:37 +08:00
eli 13432e1091 fix: drop h3 from Caddy TLS ALPN; add ws-cdn client link
Caddy only listens on loopback 8003 behind Xray REALITY fallback (pure TCP),
so advertising h3 in TLS ALPN is both useless and harmful: a client offering
h3 gets h3 negotiated over TCP and the connection hangs (verified with
openssl s_client -alpn h3,h2 -> 'ALPN protocol: h3'). Drop h3 from both
site blocks. Also print the WS CDN link (CDN site block already routes
/scilad) and assert 'alpn h3' never reappears.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 21:21:44 +08:00
eli 3c58aec356 fix: disable Caddy auto_https redirects shadowing hy2 ACME challenge route
Caddy injects runtime auto-redirect routes (host-matched, ahead of the
catch-all http:// site, not excluding ACME challenge paths) for domains
it manages certs for — invisible in static 'caddy adapt' output, so the
previous live test could not reproduce it. hy2's tokens got 308'd to 443
(LE: 'Invalid response from https://DOMAIN/...: 404' via masquerade).

Verified bidirectionally with real caddy v2.11.4: without the option the
challenge path gets 308 (exact production repro), with it 200 from hy2.
Live test now extracts the global block + http block + a tls-internal
managed site block to reproduce production conditions permanently.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 20:27:05 +08:00
eli d8269b12c1 feat: self-check probes ACME challenge route; live caddy route test
Verified with real caddy v2.11.4: fixed Caddyfile proxies
/.well-known/acme-challenge/* to hy2 (200 from mock) and 301s everything
else; the old bare-redir variant 301s the challenge (exact production
bug reproduction). tests/run_tests.sh now runs this live route test when
a caddy binary is on PATH. self_check additionally probes the challenge
path post-deploy and warns if it gets redirected.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 19:54:30 +08:00
eli 2d10c12e9f fix: default hy2 masquerade to engineersblog.net (match xray camouflage)
www.people.com answers every non-browser request with a Cloudflare JS
challenge (cf-mitigated: challenge) — the masquerade instantly fails
active probing, and the open reverse proxy gets the VPS IP flagged by
CF for scraping. engineersblog.net serves real 200 content without a
challenge; using the same target as the Xray fallback makes 443 and
8443 present the same site, which strengthens the disguise.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 19:37:53 +08:00
eli da2387dbbc fix: port-80 ACME challenge route was shadowed by bare redir
Caddyfile directive order puts redir before handle; the matcherless
'redir https://...' in the http:// block caught EVERY port-80 request
including /.well-known/acme-challenge/*, so LE's hy2 validation got a
301 to 443 (visible as 'Fetching https://...' + tls internal error) and
hysteria-server FATALed on startup. The redir now carries a
not-path matcher for the challenge prefix, making evaluation
order-independent (same pattern the main site block already used via
@notpaths).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 19:31:44 +08:00
eli ba99627efb docs: fix HTTP/3 toggle location (Speed > Settings) and dig HTTPS syntax
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 19:04:50 +08:00
eli c78cac6d8b docs: ECH is default-on for Free zones; clarify HTTPS redirect, TLS 1.2, ACM
Verified against Cloudflare docs: ECH is enabled by default on Free zones
(no toggle shown); Always Use HTTPS does not loop with Caddy's one-way
http->https redirect; Minimum TLS 1.0->1.2 is a free hardening; HTTP/3
toggle lives under Network; ACM is unrelated paid addon.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 18:56:22 +08:00
eli 72cf63b08b docs: drop Enterprise-only Origin Rule from CDN guide — Preserve works
Host/SNI/DNS-record overrides in Origin Rules are Enterprise-only (greyed
out on free plans). The architecture never needed them: CF origins with
SNI/Host=CDN_DOMAIN, Xray REALITY falls through to Caddy, and Caddy's CDN
site block matches on that SNI directly. Guide now says leave everything
at Preserve.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 18:46:32 +08:00
eli b8fba66302 docs: Cloudflare origin setup guide for current 2026 dashboard
DNS grey/orange split for REALITY vs CDN domains, Full (strict) mode,
Origin Rule (Host/SNI/DNS override to DOMAIN), ECH/Always-Use-HTTPS/HTTP3,
WebSockets toggle, common pitfalls, and minimal firewall port list.
Also dedup ECH paragraph in optional-enhancements (points to the guide).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 18:36:13 +08:00
eli d006d6455e fix: preflight silently killed the script under set -e when DRY_RUN unset
The function's last command was '[ -n "$DRY_RUN" ] && info ...', which returns
1 when DRY_RUN is empty; as the function's exit status that triggered set -e
on the very first pipeline stage — zero output, instant exit. Rewritten as an
if block, comment added, awk scan confirms no other function ends with an
&&/|| list, and a regression test executes the real preflight body with
mocked id/apt-get outside DRY_RUN.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 15:51:00 +08:00
eli 1738eb3614 docs: clarify tty detection comment
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 15:32:45 +08:00