45 Commits

Author SHA1 Message Date
eli 1d5af2248b fix(firewall): forward 链改回"作用域白名单 + 默认丢弃"
之前把 forward 链清空成 policy accept 是错的——那不是标准,是相对基线的
**安全退化**。基线(init-iptables.sh)本来就是:

    -A FORWARD -o podman+ -j ACCEPT
    -A FORWARD -j REJECT

配合 net.ipv4.ip_forward=1,清空 forward 等于让主机变成转发器。

现在按基线语义实现:

    chain forward {
        type filter hook forward priority filter; policy drop;
        ct state established,related accept
        iifname/oifname "wt0" accept      # NetBird 覆盖网
        iifname/oifname "zt*" accept      # ZeroTier 覆盖网
        iifname/oifname "podman*" accept  # Podman
        iifname/oifname "docker0" accept
        iifname/oifname "br-*" accept
    }

两处必须显式列出接口的原因(都实测过):

1. **容器发布端口的入站在 forward 里没有任何现成规则可匹配**。实测
   NETAVARK_FORWARD 只有 `saddr 10.89.0.0/24 accept`(出站)和
   `daddr 10.89.0.0/24 established accept`(回程),**没有发布端口的入站
   accept**。基线是靠 `-o podman+ ACCEPT` 放行的,这里同理用 oifname 匹配。

2. **NetBird/ZeroTier 会自己往 `ip filter FORWARD` 写规则**。实测 oraclejp
   的 `ip filter FORWARD` 里有 netbird 加的 `iifname "wt0" accept`。本表的
   `policy drop` 是终局裁决,不同步放行就会把 VPN 转发打死。

验证(oraclezyus-arm):SSH/80/443/容器 9000 全通、容器 DNS 与出站正常、
容器到宿主机 443 正常。测试 77 → 79 项全绿。
2026-11-11 12:24:44 +08:00
eli 57b21d5b99 refactor(firewall): forward 链彻底不做过滤(回到 netavark 本来就能工作的状态)
之前连续两次修错方向,根因是没搞清 nftables 的多表裁决语义:

  · netavark 自己会在 `ip filter` 的 NETAVARK_FORWARD 链里放行容器流量
    (实测:`ip saddr 10.89.0.0/24 accept`,Docker 同理有 DOCKER-FORWARD);
  · 但 nftables 的 `accept` **不是终局裁决**,包会继续流到下一张表的基础链;
  · 本表在 forward 上写 reject → 把运行时已经放行的流量打死。

第一次修:加 `ct status dnat accept`(只解决外部访问发布端口,解决不了容器出站,
因为 SNAT 在 postrouting,ct status 不是 dnat)。
第二次修:再加一堆 `iifname podman*/docker0/br-*` accept。
两次都是"补规则",越补越脆弱,还踩到 nft 1.0.2 不支持通配接口名集合的坑。

正确做法是**这一条都别加**:宿主机防火墙的职责是保护本机服务(input),
转发流量本来就该由容器运行时和云安全组负责,本表不掺和。
这是社区标准结论(Red Hat KB 5885821、netavark#977:把 FORWARD 设 DROP 就会
让 Podman/Docker 连不上)。

改动:`chain forward` 只留 `type filter hook forward priority filter; policy accept;`,
删除之前的 dnat/网桥规则。黄金基线与回归测试同步改为"forward 链内不许出现 reject/drop"。

验证:容器内 DNS 解析正常、出站 301、5 台 9000/api/health 全 200、forward 链 0 条拒绝规则。
测试 77 项全绿。
2026-11-10 13:06:25 +08:00
eli 328a41fda3 fix(firewall): forward 链放行 DNAT,否则容器发布端口全被挡成 refused
从 iptables 版迁移到 nftables 版时丢了"容器转发先放行"的语义:
  · init-iptables.sh 把 reject 追加在 FORWARD 链**尾部**,Docker/Podman 会把自己的链
    插在最前面,容器流量天然先被接受;
  · nftables 版写成 `ct state established,related accept` + `reject`,而 Podman 的
    端口发布靠 NAT 的 DNAT 改写目标地址,改写后照样要过 forward 链 → 撞 reject。

症状很坑:宿主机自己 curl 127.0.0.1 正常(走 lo),外部一律 **connection refused**。
不是超时——本表用 reject 而不是 drop,所以看着像"端口根本没开",很容易往错方向查。

暴露方式:昨天把规则推到 5 台 Oracle,把这些机器上 Podman 发布的 9000(Arcane agent
的 API)打掉了,表现是 Arcane 面板里这批 VPS 全部离线。

修法:在 forward 链的 reject 之前加 `ct status dnat accept`——只放行被本机 NAT 改写过的
包,其余转发仍然拒绝。黄金基线 dsh-firewall.nft 同步更新。

补了两条回归测试:forward 链必须有该规则,且必须排在 reject 之前。
测试 75 → 77 项。
2026-11-10 11:24:37 +08:00
eli 8fd7d9db1b feat(tools): 新增 refresh-cdn-lists.sh,把清单刷新变成一条命令
之前刷清单是我临时写内联 python 干的,没脚本、不可复现。现在补上:

- 四个来源:CF ips-v4/v6、Fastly public-ip-list、Akamai 的
  akamai_ip_cidr_blocks.lst(v4 已合并版)+ akamai_ipv6_list.lst(v6 未合并,自己 collapse)
- **交叉校验**:把 Akamai 原始 9462 行自己 collapse 一遍,与仓库给的合并版逐条比对,
  不一致就拒绝落盘(说明上游出了问题)。实测两者都是 235 条、集合完全相同。
- **确定性**:排序固定,连跑两次字节级一致(已验证 v4 脚本与 v6 文件 diff 都为空)。
- **幂等**:无差异时直接报"已是最新"退出,不碰文件。
- 默认只报告差异(`<` 当前独有 / `>` 上游独有);`--apply` 才落盘;`--cache DIR` 供离线/测试。

首次运行就抓到上游漂移:Fastly 从 19 条涨到 **21** 条,新增 90.98.0.0/17 与
90.98.128.0/18(相邻不重叠,净增 2 条 / 49,152 个地址)——这两段之前一直没被绕行。
已落盘,v4 269 → 271。

修掉一个自己引入的 bug:v6 文件的注释拼接逻辑写重了,第一次 --apply 把 193 条写成了
385 条(每条重复两次)。现在 v6 改成整文件重新生成(头和正文同源),并验证无重复。

线上已同步:两台主机 + 两个 GCP 项目的 cdn-egress-deny 规则都是 271 条,
两侧集合逐条比对完全一致。
2026-11-08 11:31:52 +08:00
eli 37db75413d fix(cdn-list): 换用上游仓库自带的合并版,并补上一直缺失的 Akamai IPv6
两件事,都是之前搞错的。

一、v4 清单的源头找错了处理方式
实际数据源是 SecOps-Institute/Akamai-ASN-and-IPs-List(按 40 个 ASN 从 RADb 拉 BGP
宣告,每小时更新)。内嵌的那份是拿它的 akamai_ip_list.lst 手工合并、且是一两年前的
快照。复核发现:
  - 该仓库本来就提供合并好的 akamai_ip_cidr_blocks.lst(235 条,与原始 9462 行
    collapse 后完全等价),根本不用自己合;
  - 旧快照已漂移:265 段 / 209,106 地址 Akamai 已不再宣告(白绕),
    49 段 / 13,504 地址是新宣告的(没绕 → 一直在按 $0.08/GiB 计费)。
换成 235 条后,v4 总数 402 → 269。

二、Akamai 的 IPv6 一直是空的
之前写"没有官方来源所以故意不收"——前提就错了:同一个仓库有 akamai_ipv6_list.lst
(3849 行,未合并,需要自己 collapse 成 183 条)。也就是说在此之前所有走 v6 的
Akamai 边缘流量都没被绕行,一直在按 CDN 计费。现已补上:v6 从 10 条 → 193 条
(CF 7 + Fastly 3 + Akamai 183)。

配套改动:
  - 脚本头部写清【怎么更新这份清单】+ CDN_LIST_AUDIT_DATE,超过 180 天打告警;
  - 测试断言从"单一区间"改成三层:总数 269±10%、CF 15 条逐条核对、Fastly 抽样;
    v6 侧补 Akamai 主段与条数下限,防止再次被删空;
  - docs/cdn-list-audit.md 重写,记录依据(GCP 按目的地网络自动判定、Akamai 无官方
    公开清单的穷举证据)、20 万地址漂移、以及 5 万条集合的实测无差异。

线上已同步:两台主机渲染结果 md5 一致(c8f16ac8…);两个 GCP 项目的 egress 拒绝规则
都改成同一份 269 段(原来是 gcoregon 旧 368 段、gcoregon-2 中间版 402 段,两边还不一致)。
真实 Akamai 边缘地址抽验命中:www.akamai.com / www.microsoft.com 的 AAAA 都落在
2600:1400::/24 内。
2026-11-08 11:17:55 +08:00
eli bb27fb24dc feat(cdn-list): v4 清单 188 → 402 条,覆盖面上补到 GCP 那份的规模
现网 188 条(CF 15 + Fastly 19 + Akamai/Linode 154)是 2026-09 审计的产物;
GCP 项目里的 egress 拒绝规则用的是更完整的一份(Akamai 368 条)。这次把主机侧
对齐到同一份,两边从此一致——不一致会导致"漏绕(按 CDN 计费)"或"误丢包"。

实测结论(写进 docs/cdn-list-audit.md):
- nft 是"一个集合一次查找",netlink 字节码里只有一条 lookup,不是几百条 cmp;
- 集合 188 条 vs 40 条端到端无差异,扩到 50,188 条仍然测不出差异;
- 所以"精简到几十条"没有任何性能收益,只会把无辜地址丢给上游 Oracle。

顺带证伪了两个之前的口头推测:
- GCP 单条防火墙规则没有 256 个 destinationRanges 上限(实测 3000 个也能建);
- 精确挖掉 Linode 反而使条数从 368 涨到 381(减法切碎片),故不挖。

测试断言从单一区间 [180,200] 换成三层保护:总数 402±10%、Cloudflare 15 条逐条核对、
Fastly 抽样核对——CF/Fastly 是官方权威值,不该漂移;Akamai 是 BGP 派生的浮动值。

两台机器已重跑并验证:CDN 出口走 Oracle,非 CDN 仍走 GCP 自己。
2026-11-08 10:38:45 +08:00
eli 5420ea95e3 feat(firewall): 主机防火墙收进本仓库,全机队共用
init-firewall.sh 之前落在 oci-toolkit 里,但它是"给任何一台代理节点上防火墙",
跟 OCI 没关系 —— GCP、阿里云、搬瓦工、Oracle 都要用同一份。挪到 vpsetup,
和 setup_*/migrate_* 放一起,按机器角色选 profile:

  proxy(默认) / hy2 / web / minimal

端口全走开关(--ssh-port/--hy2-port/--hy2-hop/--no-hy2-hop/--no-http3/
--extra-tcp/--extra-udp/--enable-frps/--enable-xmpp)。默认 profile 生成的
规则与现网 dsh-firewall.nft 逐条一致,老机器重跑是纯 no-op。

测试 tests/run_firewall_tests.sh 75 项,钉死:默认与现网一致、四个 profile 的
端口集合、任何组合都保留链尾 REJECT 兜底、非法输入 exit 2 且不产出规则。
OCI 那边同时删掉,避免两份分叉。
2026-11-08 00:48:17 +08:00
eli b1ab48afd3 docs(cdn-v6): 把 Akamai 段的空缺从 TODO 改成有依据的结论
上一版留的是"待从官方文档/WHOIS 核对后补入"。调研后发现这个待办本身不成立:
Akamai 根本没有面向公众的单文件网段清单,官方只给客户 Site Shield / Origin IP ACL
(按客户定制),第三方仓库 SecOps-Institute/Akamai-ASN-and-IPs-List 里还混着
Akamai Connected Cloud / Linode(AS63949)的客户虚拟机段,拿来当 CDN 名单会误绕一大片
普通 Linode 站点。

按仓库既有的"CDN 清单只收官方公布的网段"约束,结论是【不收】,并把理由写进文件。
对应测试从"必须含 TODO"改成"必须含留空理由 + 必须含不采用第三方的说明",防止以后
有人顺手补段却不留依据。
2026-11-07 21:05:58 +08:00
eli 15690fd9a6 fix(cdn-bypass): 改为独立实例,修掉 confdir 合并污染主配置
原方案给主 xray 加 -confdir,被 xray 的合并语义坑了两处(真机 -dump 实证):
1) 片段 outbounds 被前插成 outbounds[0],cdn-bypass-out 变成所有未命中流量的默认出站
2) Config.Override 用 c.RouterConfig = o.RouterConfig,片段的 routing 整体替换主配置的
   routing —— 5 条规则与 IPIfNonMatch 全丢(geoip:cn/私网/bittorrent/tg-in 全部失效)

单进程无解:带 routing 就毁主配置,去掉 routing 则 bypass 入站落到 direct 并打环。
改为独立 xray 实例(同一个 /usr/local/bin/xray,不重复安装):
- xray-cdn-bypass.service:User/Group 继承主 unit,CAP_NET_ADMIN 供 tproxy+SO_MARK,
  PartOf 传播 stop/restart,WantedBy 同时挂主服务(PartOf 不传播 start,
  否则看门狗次月恢复后 bypass 仍 dead,而 nft 还在打标 → CDN 流量黑洞)
- 多上游 balancer:oracle-1..N 随机选健康节点(random + observatory;
  fallbackTag 必须非空,否则 strategy 不会注入 observatory)
- 双栈:table ip / table ip6 各一套打标+tproxy,v6 tproxy 目标写死 [::1]
- 卸载顺序:先撤 nft/策略路由再停实例,避免仍在打标+端口已关

已修的真机问题:mktemp 无 .json 后缀导致 xray 拒绝加载配置;
route unit 是 oneshot+RemainAfterExit,只 enable 不会重跑 ExecStart,必须 restart;
运行时 systemctl enable --now nftables 会触发 flush ruleset 清空主机防火墙,改为只 enable。

新增 cdn-cidrs-v6.txt(只收官方段,Akamai v6 待补)与 6 条回归守卫(已做变异测试)。
2026-11-07 20:53:47 +08:00
eli a7f63052a2 fix(cdn-bypass): networkd drop-in 防 fwmark 策略路由被清——2026-10-16 gcoregon 静默失效根治
- 安装时写 /etc/systemd/networkd.conf.d/90-cdn-bypass.conf(ManageForeignRoutingPolicyRules=no),
  卸载时移除;落盘即对下一次 networkd 重启生效,装时不动 networkd。
- 根因:unattended-upgrade 升级 systemd 触发 networkd 重启,默认 ManageForeignRoutingPolicyRules=yes
  会把 route unit 加的 ip rule fwmark 1 当外源规则清掉,oneshot unit 仍 active (exited) 无人察觉。
- 已在 gcoregon 实测:写 drop-in 后重启 networkd,规则存活、CDN 出口恢复为 alius。
- 测试断言同步;顺带消除本脚本链接解析处的 SC2015。
2026-10-30 20:38:45 +08:00
eli 21afa5bfae 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 3f6e1cff21 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 4d7415e803 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 00e05c0c71 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 960ca8c3f2 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 2c13ffc03d 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 2c7c3d5566 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 1c7880609a 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 49b389c9cb 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 e786b38624 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 b43d56edb2 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 f97bb8e133 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 3b45fda3c1 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 eb7370eabb 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 50195daa10 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 f0ea7bc279 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 48e6bd4313 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 4138484128 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 51088dc7f0 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 fe59616fa5 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 f4616814b8 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 9bdd928b9b 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 384d46ee7a 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 1645e1a003 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 0f8e3eed37 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 418112b646 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 588d292ecb 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 ab908117a0 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 1b500d5a69 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 1c82fa7cef feat: self-check, client summary, uninstall
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 00:34:24 +08:00
eli 321bc96d5e feat: nftables port hopping, validation, service enablement
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 00:32:45 +08:00
eli 372b25126d feat: hysteria2 install + config render
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 00:31:13 +08:00
eli bfebe573c7 feat: xray install, keypair generation, config render
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 00:30:05 +08:00
eli d756d2aa3f feat: caddy install + Caddyfile render
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 00:27:49 +08:00
eli c7dcf88bb6 feat: parameter collection, env persistence, test scaffold
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 00:24:46 +08:00