97 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 38c2f1fd14 fix(firewall): 加 wt0 进模板、删掉无用的 NTP 规则
两项都是在 oraclezyus-arm 上单机验证得出的:

1. **加 `iifname "wt0" accept`**。NetBird 官方要求主机防火墙放行 wt0
   ("NetBird automatically adds rules to allow traffic on the wt0 interface")。
   oraclejp/oraclezyus-arm 上原本是手工 `nft insert` 的,而本脚本每次
   `delete table` + 重建,会把它洗掉——必须进模板。

2. **删 `udp sport 123`**。systemd-timesyncd 是纯客户端,实测轮询间隔 34 分钟、
   conntrack UDP 超时 120 秒,每次轮询都是新建条目,回包走 established 即可。
   实测去掉后 `NTPSynchronized` 仍为 yes。旧表里那条规则的计数器也是 0 命中。
   顺带堵掉"源端口 123 可穿透所有 UDP 端口"这个口子。

测试 77 项全绿。
2026-11-11 00:11:25 +08:00
eli a8b22b594c fix(firewall): 按旧 iptables 实现精确复刻容器放行,并补回遗漏的 NTP
用户质疑"iifname podman* accept 到底合不合理、为何旧实现只放 53"——质疑成立,
我之前把整个网桥全放行(还有 docker0/br-*),是修自己弄坏的 DNS 时顺手放开的,
没有论证过。

实测查清了容器到底需要什么:
  · 容器 DNS 走 aardvark-dns(网桥网关 :53)      → 必须放
  · oraclejp 上有容器正在连宿主机 10.89.1.1:443   → 也必须放
  · 其余端口无一使用                              → 不放

改为按端口放行(podman*/docker0/br-* 各 53 + 80/443),不再整网卡 accept。
容器要访问宿主机其它服务,显式用 --extra-tcp/udp 加。

同时补回旧实现有、新实现漏掉的 `udp sport 123`(NTP)。

关于"高端口 9993-65535":实测全队没有服务监听在该区间的公网地址上——
  · xray 的 *:58xxx / *:54xxx 是 UNCONN 通配**出站**源端口,不是监听
  · zerotier 的 23517/28019 等只绑内网地址
  · 只有 tcp/9993(zerotier)和 udp/51820(WireGuard)需要入站,已由 extra 参数覆盖
所以旧的宽范围是在裸奔,不保留。
(注:我先前汇报过 tcp 58409/38034/43149 是监听,那是我 awk 把本地+对端地址
拼接产生的假端口,已更正。)

验证:5 台容器 DNS 各 3/3 成功、出站 301、9000 入站 200、zerotier 9993 通。
测试 77 项全绿。
2026-11-10 18:53:05 +08:00
eli 4889ac36f2 docs: 删除「rules.v6 由另一个 agent 管」这条过期约束
它写在 2026-08 OCI IPv6 迁移期(当时另一个会话在写 v4/v6 对称规则),
现在已经不成立——本项目只有一个维护者。留着它会把"要不要收敛两套防火墙"
这个正当决策挡死。

替换成当前的真实状态与收敛方向,并顺带记录今天实测到的冲突:
6 台(dmit + 5 台 Oracle)同时跑着 iptables-persistent 的 ip filter / ip6 filter
和本仓库的 inet dsh_firewall,两者都挂 input hook、必须都放行才通,
所以新表只能收紧不能放开(实测:新表放行的 tcp/111 仍被旧表 REJECT)。

同步改掉 init-firewall.sh 头部和 README 里同样的措辞。
2026-11-10 15:46:47 +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 bff47139d7 fix(firewall): 补容器 DNS/出站放行、修 --local --apply 的静默失败
三处,都是 iptables→nftables 迁移时丢的语义或本地分支与远端分支不一致:

1. forward 链只放行了 `ct status dnat`(外部连容器发布端口),漏了容器**主动出站**。
   SNAT 发生在 postrouting(forward 之后),这类包的 ct status 不是 dnat,于是容器
   连外网全被 reject。实测容器内 curl 1.1.1.1 直接 Couldn't connect。
   补 `iifname podman*/docker0/br-* accept`。

2. input 链没放行容器网桥,容器内的 DNS(aardvark-dns 监听在网桥网关 :53)和
   host-gateway 访问全部被拦。实测容器内域名解析全失败。
   同样补网桥放行,另加 `ct state invalid drop`(乱序/孤儿包不该回 RST 给反射用)。

3. `--local --apply` 分支没有 set -e,且 `rm -f; exit $?` 把退出码冲成 0 ——
   nft 加载失败也会照样把坏规则持久化并返回成功,下次开机整份加载失败等于裸奔。
   改成内部 set -euo pipefail、失败即退、`rc=$?` 保留退出码、落盘用 install -m 644。

另外踩到一个 nft 1.0.2 的坑:`iifname { "podman*", ... }` 会报
"Byteorder mismatch: expected big endian, got host endian",必须拆成多条独立规则。
正是新加的 nft -c 预检在 gcoregon 上拦住了它,否则会把坏规则推上去。

验证:容器内 DNS 解析成功、出站 301、5 台 9000/api/health 全部 200。
测试 77 项全绿。
2026-11-10 11:48:48 +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 3b19271b6a feat(rotate): hy2 的 CF 令牌轮换脚本(含一次真实事故的教训)
只换 hysteria 那一路:/etc/hysteria/config.yaml 与 setup.env 里的令牌,重启
hysteria-server。明确不碰 Caddy 的 Caddyfile、docker acme-companion 的 CF_Token、
路由器的 ddns、以及历史备份——它们各自可能用别的令牌,用途拆分前不动。

第一版在这里翻过车,10 台 hysteria 全挂约 4 分钟:
  远端脚本走 stdin 喂给 `bash -s`,脚本里又用 `read` 从 stdin 读令牌,两者抢同一个
  stdin,read 读到了脚本自己的下一行,把源码片段写进了 YAML,解析失败服务起不来。
现在改成:远端脚本先 scp 落地成文件,stdin 只留给令牌。

另外补了两道保险:
  · 重启前先 yaml.safe_load 校验,坏了不重启
  · 备份之后任何一步失败(校验/重启/active/内容核对)都自动回滚回备份再重启

令牌经 stdin 传输,不出现在任何一端的 argv 里。改完是幂等的:再跑一次只会说"已是新令牌"。
2026-11-10 10:42:45 +08:00
eli 6982948324 docs(readme): 补 init-firewall.sh 章节(之前脚本在、README 里完全没提)
README 的索引和选型表里都没有它,等于新人只知道有四个脚本。补上:

- profile 表 + 端口开关清单
- 部署到新机器的最小步骤,以及脚本内置的 SSH 自检说明
- **多机差异化怎么判断**:只看 `ss` 里 0.0.0.0 / * / [::] 监听什么,
  绑 127.0.0.1 的一律不管。把当前所有对外端口全放进白名单 = 功能零变更,
  这是上生产最稳的推进方式(先做到行为不变只加纵深,再考虑收窄)
- 两个坑:会自选端口的服务(netbird)重启后可能换端口被挡;
  `ss -uln` 里的随机高端口多半是出站 socket,别往白名单里加

以及不变式重申:只 nft -f 自己的文件,永不跑整份 /etc/nftables.conf,
不碰 rules.v6,独立表不动别人的表。
2026-11-08 20:40:25 +08:00
eli dea51c546a chore(cdn-v6): 文件头补上 Akamai 来源仓库地址(测试断言要求写明来源) 2026-11-08 13:13:39 +08:00
eli ca5d8ee84a refactor: CDN 清单生成器移交 gc-toolkit/cdn-list
这份清单存在的唯一原因是 GCP 把 CDN 出站单独计价(Carrier Peering,$0.08/GiB 从
第一字节起算、无 200 GiB 免费额度),而消费它的 VPC 拒绝规则 cdn-egress-deny 本身
也是 GCP 资源。所以生成器归 gc-toolkit 更合理。

- 删掉本仓库的 tools/refresh-cdn-lists.sh(功能已合并进 gc-toolkit,避免两份分叉)
- 脚本头部与 docs/cdn-list-audit.md 改成指路 gc-toolkit
- 清单内容本次无变化:v4 271 条 / v6 193 条,与 gc-toolkit 生成的 canonical 文件一致

迁移后 gc-toolkit 那边多了两件事:生成 canonical 的 cdn-cidrs-v4/v6.txt,
以及 --vpsetup 参数(一次运行同时同步本仓库的内嵌清单与 v6 文件)。
2026-11-08 13:12:01 +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 b4a3640536 docs(guard): 记录 2026-11-07 用 GCP Monitoring 做的实测校准
拿 compute.googleapis.com/instance/network/sent_bytes_count 跟本站 tx_bytes 增量法在同一
时间窗口对账:4,096,497,518 vs 4,137,694,034 bytes,差不到 1%。结论是计量手段本身够准,
偏差主要来自 set-used 的初始估计,不是方法问题。

顺手把"怎么查真实用量"写进头注释(gcloud 没有 time-series 子命令,得打 Monitoring v3
REST API 并自己把 delta 点求和),下次校准不用重新摸索。同时标注该指标比计费口径宽,
是上界。

gcoregon 的 set-used 已按实测从 12 GiB 校正到 9 GiB。
2026-11-07 21:04:16 +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 3d35d0381c feat(guard): 新增月度出站流量看门狗
为 GCP Standard Tier 每月 200 GiB 免费出站额度设计的守护脚本:
- traffic-guard.sh:统计非 lo、UP 网卡的 tx_bytes,达阈值后停代理服务(绝不关机),
  次月自动恢复且只恢复它自己停过的服务;status / set-used / resume 子命令
- setup_traffic_guard.sh:安装为 oneshot service + 5min timer,配置写 /etc/traffic-guard.env
- 手动执行时也读 /etc/traffic-guard.env(sed 精确取 KEY="value",不 source)

默认阈值 185 GiB(留 15 GiB 余量),账期按 America/Los_Angeles 自然月。
2026-11-07 20:53:39 +08:00
eli 0cc1d093b9 docs: 替换 AGENTS/README 真实域名/IP,改用代号与占位符;更新测试断言与夹具 2026-11-04 19:07:29 +08:00
eli c3d06b2273 docs: cdn-bypass networkd drop-in 三处文档同步;HANDOFF §13 记录 10-16 事故与 10-30 修复实测;README 287→188 段订正;顺带落库 §12(10-24 ZeroTier 恢复,此前漏提交) 2026-10-30 20:38:45 +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 f0b4c1a984 style: vlessenc 解析处 A&&B||C 改 if(SC2015)——本地 shellcheck 0.8.0 默认 severity 闸门恢复全绿 2026-10-30 20:38:45 +08:00
eli 4a88b3a4ac AGENTS: point workspace to /home/eli/cloud/vpsetup 2026-10-25 10:20:36 +08:00
eli 817238206e Never start nftables at runtime: enable --now flushes the host firewall
On a fresh Ubuntu 24.04, systemctl enable --now nftables starts the unit,
which runs the whole /etc/nftables.conf including flush ruleset, wiping
every iptables-persistent INPUT/FORWARD rule until the next reboot.
Enable only (boot ordering nftables@sysinit -> netfilter-persistent@
multi-user keeps the flush harmless); the hysteria table is still loaded
individually via nft -f below.
2026-10-25 10:20:36 +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 cd26ae505f 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 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 8c8cb05e0a 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 9722fa1825 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 06d26b252b 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 582f12efaa 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 e23f5a7c1a 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 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 09a5f6566a 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 5916d1e1e1 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 0bd50a3157 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 两端一致即可用(f0ea7bc 起链接内嵌),
  原注释仍停留在 48e6bd4 的结论

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-26 15:48:28 +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 4740f67ddc chore: gitignore 加入 .env(Northflank API token 等本地机密,不入库)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-26 15:31:15 +08:00
eli 4fe71e323d 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 826014e3b8 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 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