35 Commits

Author SHA1 Message Date
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 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 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 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 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 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 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 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 eb9a0c90ca feat: ACTION=upgrade — non-interactive upgrade of all components
Install is already idempotent-upgrade (apt install caddy / xray install script /
get.hy2.sh all fetch latest); upgrade mode additionally requires an existing
setup.env (dies instead of accidentally installing) and never prompts.
Legacy setup.env without HY2_CERT_MODE infers dns when CF_API_TOKEN present,
so upgrades keep wildcard certs instead of silently switching to http mode.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 15:27:32 +08:00
eli 31facd9c7a feat: optional VLESS post-quantum encryption (XRAY_VLESS_ENC), README index + ECH/PQ docs
- XRAY_VLESS_ENC=1 generates ML-KEM-768 keypair via xray vlessenc, applies
  to all three VLESS inbounds, share links carry client encryption param,
  keys persisted in setup.env
- README: project intro + script index at top, optional-enhancements section
  (VLESS PQ, ECH for CDN_DOMAIN, hy2 obfs/mimic pointers)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 15:20:15 +08:00
eli e41a5b5ab3 feat: hysteria2 HTTP-01 standalone mode binds port 80 when Caddy skipped
SKIP_CADDY=1 + HY2_CERT_MODE=http now checks port 80/TCP is free (ss) and
lets hy2 answer HTTP-01 directly (altPort 80, all interfaces) instead of
erroring out; still dies if 80 is occupied.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 13:28:27 +08:00
eli 70a19d126c feat: default hysteria2 cert to ACME HTTP-01 via Caddy port-80 proxy
hy2 certmagic HTTP-01 listens only on 127.0.0.1:9180 (altPort); Caddy's
port-80 block reverse-proxies /.well-known/acme-challenge/ to it. Non-wildcard
DOMAIN cert, auto-renewed and hot-applied, no DNS API token needed, no new
public ports. TLS-ALPN is unusable (CA only connects to TCP 443 = Xray) and
reusing Caddy's cert files needs permission/copy hooks, so HTTP-01 is the
minimal stable default. HY2_CERT_MODE=dns keeps Cloudflare DNS-01 wildcard
as fallback (CF_API_TOKEN required only in dns mode).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 12:57:49 +08:00
eli 922a397e70 feat: self-check, client summary, uninstall
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 00:34:24 +08:00
eli 7d9524ac87 feat: nftables port hopping, validation, service enablement
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 00:32:45 +08:00
eli 97a3d9dcc2 feat: hysteria2 install + config render
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 00:31:13 +08:00
eli 45552433a9 feat: xray install, keypair generation, config render
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 00:30:05 +08:00
eli d616d751dd feat: caddy install + Caddyfile render
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 00:27:49 +08:00
eli e0c3341219 feat: parameter collection, env persistence, test scaffold
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 00:24:46 +08:00