Skip to content

加固(P1): 热切换键序无关比较 + 版本比较健壮 + 抑制闩兜底 + 订阅 SSRF/空结果/Host#54

Closed
Sway-Chan wants to merge 23 commits into
dododook:mainfrom
Sway-Chan:up-7
Closed

加固(P1): 热切换键序无关比较 + 版本比较健壮 + 抑制闩兜底 + 订阅 SSRF/空结果/Host#54
Sway-Chan wants to merge 23 commits into
dododook:mainfrom
Sway-Chan:up-7

Conversation

@Sway-Chan

Copy link
Copy Markdown
Collaborator

栈在 #48#53 之上,建议最后合并;前序合并后本 PR diff 自动收敛为仅 P1。

独立 review 发现的 P1 加固。

改动

  1. canHotSwitch 键序无关比较:stableStringify 替代 JSON.stringify,避免渲染层重建配置键序不同误判退回整重启。
  2. compareVersions 健壮:容忍 v 前缀 + prerelease/build 后缀、NaN→0。
  3. 核心更新抑制闩兜底计时器:无首启事件时到期解除自动重启抑制。
  4. 订阅 SSRF 兜底:限 http(s) + 拒字面私网/回环/link-local/CGNAT/云元数据 IP。
  5. 订阅空结果区分:"格式不识别"vs"URL 解析失败"。
  6. JSON 订阅 ws Host 折叠:与 httpupgrade 对齐,修 JSON-vs-分享链解析不一致。

验证

npm run build:main + eslint 通过;独立 review 收口(2 处发现已修复+验证)。

🤖 Generated with Claude Code

Sway-Chan and others added 23 commits June 8, 2026 16:44
阻止 QUIC 的 reject(UDP 443) 规则原本只放在路由末尾,但 smart 模式下
"google 关键词→代理"和"geosite-!cn→代理"两条规则在它之前且是终止规则,
国外 QUIC 在被路由到代理时即终止,走不到末尾的 reject —— 导致 smart 模式
下 Block QUIC 只拦到不在任何 geosite 列表里的未知域名 QUIC。(global 模式
无 geosite 规则,末尾 reject 可兜全部,不受影响。)

改为给每条"→代理"规则各配对一条放在其之前的 UDP-443 reject,使代理向
QUIC 在被路由前就回退 TCP;CN 直连规则(geosite-cn/geoip-cn)不配对,故
CN/直连 QUIC 保持直连不拦。末尾兜底 reject 保留(global 模式 + smart 模式
未被 google/!cn 命中的代理向 QUIC)。三条 reject 共用新增的 blockProxyQuic
协议 guard(跳过 hysteria2/tuic,其传输走 QUIC dial-server)。

注: 上游合并 dododook#45 时未包含此 smart 模式修复,故对 v3.5.1 重新提交。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
两个问题合并修复:

1) 订阅解析丢节点(数据缺失 bug)
   SubscriptionService 的 sing-box outbound 白名单 SUPPORTED 漏掉 naive,
   导致订阅里的 naive 节点被静默跳过——真实订阅(29 个 naive 节点)拉取后
   全部消失。补入 naive 分支:解析 username/password/tls,并按 quic 字段
   推导 useHttp3。

2) naive HTTP/3(QUIC)支持
   sing-box 的 naive outbound 通过 quic:true 走 h3(QUIC/UDP) 传输,对应
   服务端 `--listen=quic://`;此前 FlowZ 从不下发该字段,naive-h3 节点被
   当成 H2/TCP 拨号。新增 NaiveSettings.useHttp3:
   - 生成侧:useHttp3 时给 naive outbound 注入 quic:true;
   - blockQuic guard:naive-h3 属 UDP-transport(QUIC dial-server),
     reject UDP443 会经 strict_route 回流误杀其传输,故并入跳过集合
     (原仅 hysteria2/tuic)。抽出 isUdpTransportNode 统一判定;
   - UI:naive 表单新增 HTTP/3 开关 + 中英文案;
   - 类型:ServerConfig.naiveSettings、SingBoxOutbound.quic、订阅侧
     SingboxOutbound.username/quic。

验证:sing-box 1.13.3 check 差分确认 naive 的 quic 为合法 bool 字段
(unknown-field / 类型错误对照),quic:true 配置解析通过;tsc/eslint/
prettier/vite build 全绿,无新增类型错误。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
根因:blockQuic guard 原以"拨号传输"(isUdpTransportNode: hy2/tuic/naive-h3)判定跳过,
混淆了两个正交属性——决定"代理向 QUIC 会不会黑洞"的是"节点能否中继客户端 UDP",而非
节点拨号用什么传输。naive 拨号虽可为 h3(QUIC),但它只过 TCP(HTTP CONNECT)、不能中继
客户端 UDP,被错误归入"UDP-transport 跳过"集 → 开 blockQuic 时其代理向 QUIC 仍走代理 →
naive 收不了 → 黑洞(影响用户 29 个 naive-h3 节点)。

实测定论(netns 隔离 TUN,auto_route+strict_route,抓包差分):
- 带 `reject udp443` 时 hy2 拨号包仍正常逸出(14 vs baseline 10);
- 客户端 udp443 被 reject(egress 0 vs 控制 udp444 1)。
证伪 PR-1"reject 经 strict_route 回流误杀拨号"的假设——拨号是 sing-box 进程自有 fwmark
socket,受 auto_detect_interface 保护、绕过 route 规则,reject 只作用于 tun-inbound 客户端流。

改为按 isUdpRelayCapable 分流:
- 不能中继(ssh/http/anytls/naive;naive 仅在 udp_over_tcp 下可中继,FlowZ 不下发该字段):
  恒 reject 全部代理向 UDP(逼 QUIC 回退 TCP、其余 UDP 快速失败而非穿透 final 跌 direct 假死 30s);
- 能中继(hy2/tuic 原生;vless/vmess/trojan/ss 经 UoT):仅 blockQuic 开时 reject 代理向 QUIC(udp443)。

连带修两个 bug:
1. naive 漏在原 tcpOnly 集 ['ssh','http','anytls'] → 代理向 UDP 黑洞;补入。
2. tcpOnly 全-UDP reject 原仅在路由末尾,smart 模式下 geosite-!cn→代理 为终止规则先命中、末尾
   reject 够不着 → 给全-UDP reject 加 smart 配对(每条 →代理 之前),与 PR-1 对 blockQuic 的修法一致。

本提交取代 PR-1/PR-3 引入的 isUdpTransportNode 临时 guard;naive 生成侧注释同步更正。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 把"节点域名/IP → direct"豁免从"仅选中节点"扩展到"全部节点",使切换节点 / detour 前置
  代理无需重生成配置即被豁免(为无缝切换铺路)。
- CDN 安全:域名节点仅用 domain + domain_suffix(靠 sniff 出的 SNI 精确匹配节点域名),
  去掉过宽的 domain_keyword(会误匹配任意"含该域名串"的无关域名);不把域名预解析为共享
  CDN IP(共享 IP 加直连会误伤同 IP 的被墙站点、且抗不住 IP 轮换)。仅 IP-literal 节点用
  ip_cidr 排除(专用 IP、安全)。

已知遗留(待真机验证,已在代码注释标注):Windows TUN 的 resolvedIps 预解析仍把域名节点
解析为 IP 加 route_exclude_address——即 Windows 上的 CDN 风险点。是否可删需 Wintun 真机验证
"域名规则 + sniff 是否足以断回流环";若 Windows 确需 IP 级兜底,应仅对 IP-literal 节点保留。
无 Windows 环境暂不擅改。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
落实设计纠正——「禁 QUIC」的实现应只有一条 udp443 reject,且与选中节点无关。

删除的非预期实现:
- rejectProxyUdpAll(reject 全部代理向 UDP):超出「禁 QUIC(443)」范围,会误砍能中继节点的
  游戏/WebRTC 等非 QUIC UDP;
- isUdpRelayCapable / TCP_ONLY_PROTOCOLS 节点分类;
- 连带:blockQuic 不再跳过 hy2/tuic——concern-A 实测已证伪「reject 杀拨号」,无需跳过。

实测依据(netns 隔离 TUN,零宿主改动):UDP 路由到不能中继的出站时,sing-box 在出站层直接
拒绝(日志 "UDP is not supported by outbound"),不漏 direct、不黑洞。故非 QUIC 的代理向 UDP
无需路由层按节点固化 reject——FlowZ 旧注释「穿透 final 跌到 direct 假死 30s」系未验证的错误
前提(与 concern-A 同源)。

收敛后:
- blockProxyQuic = config.blockQuic === true && !!selectedServer(节点无关);
- smart 模式每条 →代理 前配对、以及末尾兜底,均只 reject udp443;
- hy2/tuic 自身 QUIC 拨号受 fwmark 保护、不受影响(已实测)。

红利:路由配置与选中节点解耦 → 为 PR-5 selector 跨协议无缝热切换消除规则失配隐患。
本提交取代 PR-2 的「中继能力分类」模型(PR-2 未推送,最终提交时建议与 PR-1/PR-2 合并为一条
「节点无关 Block QUIC」PR)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
切节点从「重启 sing-box(断流)」改为 clash_api 热切换 selector,默认不断流(优雅切换)。

出站生成(generateOutbounds):
- 改为生成【全部】节点出站(逐节点 try/catch 防单点拖垮)+ 一个 selector(tag=proxy-selector,
  列出全部节点、default=当前选中、interrupt_exist_connections=用户开关)。
- 路由 final 与「→代理」规则、DNS detour 统一指向 proxy-selector,与选中节点解耦——PR-2 简化已
  使 UDP 规则节点无关、PR-4 已豁免全节点域名,故跨协议热切换零规则失配。

切换链路:
- CONFIG_CHANGED 监听 → switchMode 智能分派:当且仅当唯一变化是「切到运行中 selector 内的节点」
  → clash_api `PUT /proxies/proxy-selector` 热切换、持久化 selectedServerId、不重启;失败退回重启;
  其余变化(模式/端口/TUN/规则/节点集合/interrupt 开关)仍重启。canHotSwitch 严格校验。

开关:
- UserConfig.interruptConnectionsOnSwitch(默认 false=优雅,连接保留至自然关闭)→
  selector.interrupt_exist_connections;高级设置新增开关 + 中英文案。

连带修复 / 一致性:
- 修复旧代码多跳 detour 链断裂(旧逻辑仅设选中节点 detour、链中节点 detour 丢失)→ 逐节点设
  detour + 内联环检测(成环跳过、避免 sing-box 循环引用启动失败);
- 删除因此变为死代码的 getDetourChain;
- Shadow-TLS 节点反查改为按 idToTagMap 名称匹配(适配全节点名 tag);
- 校正 blockQuic UI 与节点无关后端一致:移除对 hy2/tuic 的禁用(isQuicNode useMemo)+ 文案"不适用
  Hysteria2/TUIC"(concern-A 实测已证伪跳过依据)。

验证:sing-box check 通过 selector(interrupt false/true 两态);tsc/eslint/prettier/vite build
全绿;渲染 tsc 错误数维持基线 10、PR-5 改动文件零新增类型错误。
注:「热切换真不断流」属运行时功效,需真机验收(本机无 GUI/host-TUN)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 个抗封特性的「类型 + 订阅解析 + 生成 + sing-box check」(运行时抗封效果需真机验收):
- ECH(tls.ech.enabled):隐藏 SNI;
- Multiplex(multiplex,h2mux/smux/yamux + padding):reality+vision(xtls-rprx-vision) 与 mux
  不兼容 → 生成侧 guard 自动跳过;
- TLS Fragment(tls.fragment):切分 ClientHello 抗 SNI-DPI。提供全局开关(高级设置,对所有 TLS
  节点生效)+ 每节点字段;
- httpupgrade 传输(transport.type=httpupgrade,复用 ws 的 path/Host);
- Hy2 端口跳跃(server_ports/hop_interval,支持多段逗号分隔范围)。

实现:
- 生成统一后处理 applyAntiCensorshipOptions(ECH/每节点 Fragment/Multiplex/Hy2 跳跃),避免散落到
  各协议的 tls 构建点;httpupgrade 在 generateTransportConfig;全局 Fragment 在 generateOutbounds
  后处理所有 TLS 节点出站。
- 订阅 SubscriptionService 解析以上字段,使订阅节点自动携带(主入口)。

字段确切性:5 项逐一 sing-box check 验证合法字段(确认是 tls.fragment 而非 outbound 级 tls_fragment);
五合一 check(含在 PR-5 selector 内)通过。

UI 覆盖说明:全局 TLS Fragment 开关有完整 UI(纯客户端、非订阅来源);ECH/Multiplex/httpupgrade/
Hy2-跳跃 经订阅自动解析+生成已完整可用,手动节点表单 UI 作为后续补充(用户节点来自订阅)。

验证:tsc/eslint/prettier/JSON 全绿;渲染 tsc 维持基线 10;PR-6 文件零新增类型错误。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
HIGH
- [H1] app/custom 分流的「→代理」终止规则补配对 udp443 reject(原 blockQuic 对这些域名失效);
- [H2] 托盘切节点改走 switchMode(原直接 stop+start 硬重启断流);
- [H3] cache_file 会持久化 selector 的 clash_api 选择、重启后覆盖 config default(已实测证实)→
  启动后用 clash_api 把 selector 校正回 config.selectedServerId,让 FlowZ 配置成单一真值。

MED
- 节点域名绕过去掉裸 apex:domain_suffix 仅 `.${d}`,防止把共享 apex 下别的真实站点沉降到直连;
- blockProxyQuic 门改判代理可用性(proxyMode!=direct && servers.length>0),不再依赖 selectedServer
  解析成功(避免选中失效但 selector 仍出流量时 QUIC 漏过);
- tls.fragment 排除 hy2/tuic/naive:实测 naive 出站拒绝 fragment 字段("fragment is not supported
  on naive outbound" → 启动 FATAL,h2/h3 皆然);hy2/tuic 的 TLS 在 QUIC 内、无 TCP ClientHello;
- multiplex 的 vision guard 只看 flow 含 'vision'(去掉 reality 条件——普通 TLS+vision 同样与 mux 不兼容);
- 仅含 server_ports 的 Hy2 端口跳跃节点不再被订阅解析丢弃(从首段范围低位推导 port);
- canHotSwitch 对 servers 按 id 归一化比较(订阅更新仅重排节点不再触发多余重启)。

LOW
- clash_api PUT 加 2s 超时(AbortSignal.timeout);stop() 清 currentIdToTagMap 防对已停 selector 误切。

验证:tsc/eslint/prettier 全绿;渲染 tsc 维持基线 10;naive-fragment、selector、五特性合一 sing-box
check 均通过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
HIGH
- canHotSwitch 重写为「对齐 selectedServerId + servers 按 id 归一化后整体深比较」:
  · 修「切节点 + 改某设置」同时发生时那个设置被静默丢弃——原 hasModeChanged 白名单没覆盖
    blockQuic/tlsFragment/dnsConfig/allowLan/bypassLAN/mixedPort/各 TUN 子字段 等;深比较后任何
    非 selectedServerId 的差异都退回重启。
  · Windows TUN 一律退回重启:route_exclude_address 在 start 时按「选中+appRules」节点构建、
    热切换不重生成配置无法补排除集,否则 Wintun 把节点自身出向包回捕进 TUN 成环。
  · 移除被取代的死方法 hasModeChanged。

MED
- reassertSelectorSelection 每轮读最新 currentConfig.selectedServerId:避免启动窗口内用户已热切
  到别的节点时、reassert 把它 revert 回启动时的旧节点;
- getUniqueTag 预占 proxy-selector/direct/block/direct-loopback 保留 tag:防用户把节点命名为这些
  导致 tag 撞车启动 FATAL。

LOW
- 全节点生成失败时提前抛清晰错误(避免空 selector 触发含糊 FATAL);
- naive useHttp3Desc 文案更正:节点自身 QUIC 拨号受保护、浏览器到该节点的 QUIC 仍按 Block QUIC
  处理(原文案"不受 Block QUIC 影响"误导)。

验证:tsc/eslint/prettier/JSON 全绿;渲染 tsc 维持基线 10;naive h2/h3 拒绝 fragment 已实测确认
(全 naive 排除正确)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
根因:FlowZ 的 sing-box 编进了 cronet-go 绑定,但运行时靠 purego 从「核心同目录/系统库路径」
加载 libcronet,而 resources/ 未带该库 → 选中 naive 节点必报 "cronet: library not found"、连不上。
与 PR-5 的「全节点 selector」叠加更严重:sing-box 启动会预初始化全部出站,任一 naive 节点缺库即
整体启动 FATAL(连非 naive 节点也用不了)。

打包(与 sing-box 同模式:resources 随 extraResources 的 `**/*` filter 打进安装包):
- scripts/fetch-cronet.mjs:按平台下载 libcronet(linux/win/mac-arm64/mac-x64) → resources/{平台}/,
  版本 CRONET_VERSION 应与打包 sing-box 的 cronet-go 对齐(cronet 走 Chromium C API,跨小版本一般兼容);
- package.json 加 fetch:cronet,并接入 release:prepare(打包前先拉库);
- libcronet 体积大、.gitignore 不入库,由 CI/维护者 fetch 后随包打入(区别于较小的 sing-box 直接入库)。

运行时定位:
- ResourceManager 加 getCronetLibFilename / hasCronetLib / ensureCronetBeside;
- ensureWritableCore(Linux 写可写核心) 与 CoreUpdateService(核心单独更新写盘后) 都把 libcronet 拷到
  核心同目录——官方 sing-box 包不含 libcronet,naive 始终依赖随 app 打包的那个;mac/win 非便携从
  resources 同目录天然可见。

防 brick:缺库时 generateOutbounds 跳过 naive 节点并告警,保证用户仍能用其它协议节点。

验证:tsc/eslint/prettier 全绿;fetch 脚本 node --check 通过。运行时(libcronet 加载、naive 真连通)
需在带库的真机/打包环境验收——本机下载该原生库被安全门拦截、无法端到端实测。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
HIGH
- [H1] fetch-cronet 去掉 mac 目标:cronet-go 不发 darwin 预编译库(实测最新 release 0 个 dylib),
  原脚本两个 mac 目标 404 → process.exit(1) 让 release:prepare 的 && 链中断、所有平台都打不出包。
  mac naive 需用 cronet-go build-naive 从源码构建 libcronet.dylib,否则 hasCronetLib()=false 优雅跳过。
- [H2] 被跳过的 naive 节点仍被 app/custom 分流规则按 tag 引用 → sing-box "outbound not found" 启动
  FATAL,没真正防住 brick。改为 generateSingBoxConfig 末尾统一把指向「无效出站」的 route.outbound
  修正为 proxy-selector(覆盖所有死引用来源)。

MED
- [M1] 选中节点是被跳过的 naive → 不再静默切到 nodeTags[0],改为明确抛错(提示选其它协议节点);
- [M3] hasCronetLib 改查「sing-box 实际加载目录(dirname getSingBoxPath)」而非内置 resources 目录——
  否则 ensureCronetBeside 拷贝失败时仍误判"可用"、naive 不被跳过 → 找不到库 FATAL;
- [M4] 手动替换核心 replaceManualCore 补 ensureCronetBeside,与自动核心更新路径一致;
- [M5] fetch 原子写(.tmp + rename)+ 失败删残,避免截断文件被下次当"已存在"误用;
- 抽出 isNodeUsable 统一判定(generateOutbounds 跳过 / M1 / H2 共用)。

验证:tsc/eslint/prettier 全绿;fetch node --check 通过;渲染 tsc 维持基线 10。运行时(libcronet
加载、naive 真连通、mac 限制)仍需带库真机验收。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
实测 sing-box 二进制:mac-arm64 含静态 cronet 符号(1219 处)、无 dlopen
libcronet.dylib —— cronet 由 CGO 静态编入,naive 开箱即用、无需外部库;
mac-x64 二进制未含 cronet(0 处)→ naive 不可用。linux/win 仍走 dlopen
动态库。原 hasCronetLib 在 mac 查 .dylib 必为 false → 误跳 arm64 上本可
用的 naive 节点(甚至选中时抛错)。

- hasCronetLib:darwin 改按 arch 判定(arm64=静态可用,x64=不可用),
  不再查 .dylib;linux/win 维持查同目录动态库。
- fetch-cronet.mjs:更正 mac 注释(静态编入、非源码构建),mac 不在下载
  范围。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 功能特性列表新增:无缝切换节点、Block QUIC(节点无关)、抗封增强(Fragment/ECH/Multiplex/
  httpupgrade/Hy2 端口跳跃)。
- 新增「抗封 / 切换 / NaiveProxy 说明」章节:
  · 无缝切换默认优雅不断流 + 「切换时中断连接」开关语义;
  · Block QUIC 行为(节点无关、只禁代理向 udp443、拨号受保护);
  · 抗封增强各项来源(全局开关 vs 订阅自动识别)+ 排除规则;
  · ⚠️ NaiveProxy 需 libcronet、打包方式、以及 macOS 无官方预编译库的限制(缺库时 naive 节点
    自动跳过)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
解决"核心更新后配置解析不支持导致 brick"的健壮性问题:

- 预检(替换前):新核心二进制可执行 + 用「针对新版本生成的」FlowZ 配置跑
  sing-box check;不通过则不动现役核心、代理继续运行,并标记问题版本。
- 自动回滚:更新后新核心首次启动失败(proxy 'error')→ 回滚到备份核心并以
  旧核心重启;标记问题版本防再次自动安装。
- 问题版本跳过:预检/启动失败的版本记入 known-bad,checkUpdate 自动跳过
  (手动更新可强制安装)。
- 备份生命周期:更新后保留旧核心备份作回滚网,新核心「首次成功运行」
  (recordSuccessfulVersion)即删除备份——稳定即不再回滚、省磁盘。
- 兼容版本带闸门:默认仅自动更新到配置生成器已验证的 minor 带(≤1.13);跨
  minor(如 1.14)不自动更、提示随 App 升级。设置可关(手动更新不受限)。

实现:CoreUpdateService(预检/known-bad/备份/自动回滚/带闸门)+ ProxyManager
.buildPreflightConfigJson(按目标版本生成预检配置,复用 coreVersion 分叉)+
index 'error' 钩子 + UserConfig.restrictCoreUpdateToCompatibleMinor 开关
(设置 UI + 中英 i18n)。

实测:sing-box check 正确接受 1.13.x 全特性配置、拒绝跨版本不兼容字段
(inbound sniff);build:main / vite build / tsc / eslint / prettier 全绿。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- #1 版本带闸门 parseFloat 误判(HIGH):改整数编码 major*1000+minor 比较,
  修 "1.100"→1.1 漏放、"1.9"→1.9 误判等多位 minor 塌缩问题。
- #2 自动回滚被自动重启风暴延迟(HIGH):待验证窗口内 ProxyManager 抑制自动
  重启(setAutoRestartSuppressed),新核心首次异常退出立即上报 error → 立即
  回滚,而非在已知坏核心上空转 MAX_RESTART_COUNT 次后才回滚。
- #3 1s 假成功删备份致 brick(HIGH):'started' 仅代表存活 1s,不再立即删
  备份;改启动 30s 稳定观察期,期内无 error 才删;期内崩溃→回滚(备份仍在)。
- #4 陈旧 pending 误回滚(MED):pending 加更新时间戳,超 5 分钟未成功运行的
  error 不再触发回滚(视为与本次更新无关,如用户期间改了配置)。
- #7 问题版本永不解除(LOW):版本成功运行后从 known-bad 移除,误标记可恢复。
- #9 exec 注入面(LOW):预检改 execFile(无 shell),消除引号转义类问题。
- 附带(review 外自查):预检在备份之前,原 catch 的 restoreBackup 可能误恢复
  陈旧 .bak 致静默降级 → 加 backupMade 守卫,仅本次确已备份才恢复。

tsc/eslint/prettier 全绿;encodeMajorMinor 各边界(1.13/1.14/1.20/1.100/1.9/
2.0)实测判定正确。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
三核统一到官方 1.13.13(with_naive_outbound),消除版本漂移
(原 linux 1.13.3 / win 1.13.6 / mac-arm64 1.13.11)。mac-x64 按当前
编译保持 1.12.13(FlowZ 仅出 arm64 包;Intel naive 暂不纳入)。

libcronet:fetch-cronet 钉的 v148.0.7778.96-1 实测与 1.13.13 内置
cronet(cronet-go ade33496)字节一致(sha256 dc7293a9…),ABI 匹配,
不需改 fetch-cronet。

实测验证:
- sing-box check 通过 FlowZ 1.13.x 全特性配置(selector / naive /
  hy2 跳端口 / tls.ech / tls.fragment / multiplex / httpupgrade /
  block 出站 / route action:sniff)。
- naive 出站运行时 dlopen cronet 正常(成功初始化引擎并拨号)。
- 1.13.13 按预期拒绝 inbound sniff(removed in 1.13.0),FlowZ
  coreVersion<1.13 分支 guard 正确,升级后仍生成 1.13.x 风格配置。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
紧接前一提交的三核统一,把 mac-x64 也升到官方 1.13.13 darwin-amd64
(with_naive_outbound + lib/darwin_amd64 静态 cronet),不再留 1.12.13。

- resources/mac-x64/sing-box → 1.13.13(静态 cronet,Intel mac naive 可用)
- electron-builder.json:mac target arch 加 x64 → arm64/x64 一并打包发布
- ResourceManager.hasCronetLib:darwin 改为恒 true(两 arch 核心均静态含
  cronet,naive 皆可用),移除原 arch-gate
- ProxyManager:更新 1.12.x sniff 分叉注释(打包核心现全 ≥1.13.13,该分支
  仅为向后兼容旧 userData 核心保留)

至此四平台核心统一 1.13.13;naive 在 mac 两 arch + linux + win 全可用。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
人工配置开放 ECH/Multiplex 的渲染层共享组件。各表单 schema/默认值/submit 仍各自维护
(RHF 限制),渲染统一走此组件避免多表单重复 JSX。MultiplexFields 内置 disabled+提示
(vision flow 不兼容时由调用方传入)。后端 applyAntiCensorshipOptions 已消费对应字段,
本组件只补人工入口。

[WIP] 待接入 vless/trojan/vmess/ss/tuic/hysteria2/anytls 表单 + i18n。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
把原来仅订阅自动识别的抗封字段开放到各协议人工表单(additive,零后端改动——
ProxyManager.applyAntiCensorshipOptions 已消费这些 ServerConfig 字段):

- ECH:vless/trojan/vmess/tuic/hysteria2/anytls(TLS 段下方开关)
- Multiplex(协议 + max/min + padding):vless/trojan/vmess/ss;vless 按
  flow=xtls-rprx-vision 禁用+提示(镜像后端 vision guard)
- httpupgrade 传输:vless/trojan/vmess(network 枚举 + 复用 ws path/host)
- Hysteria2 端口跳跃:serverPorts(逗号分隔范围) + hopInterval

新增字段即共享:shared/anti-censor-fields.tsx(EchField/MultiplexFields)避免
多表单重复 JSX;各表单 schema/默认值/submit 仍各自维护(RHF 限制)。i18n 中英
各补 15 键。协议级限制靠"只接支持的表单",flow 级靠组件 disabled+提示。

vite build / eslint / prettier / JSON 双语 全绿。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 个协议表单的 TLS/传输/Reality/基础字段渲染存在大量重复 JSX。本次抽出 4 组
共享渲染组件(schema/默认值/submit 仍各表单自持,仅渲染共享),9 表单接入,净删
约 780 行。

新增组件:
- shared/basic-fields.tsx: AddressField / PortField(placeholder 必传)
- shared/tls-fields.tsx: TlsServerNameField(label/desc/placeholder/optional 可配) /
  FingerprintField / AllowInsecureField / AlpnField(placeholder 必传)
- shared/transport-fields.tsx: WsPathField / WsHostField
- shared/reality-fields.tsx: RealityPublicKeyField / RealityShortIdField

接入:vless/trojan/vmess/ss/hysteria2/tuic/anytls/http/socks。
豁免内联:naive(全表 text-foreground 标签主题,分片抽离会造成视觉不一致)、
ssh(独有 grid-cols-3 + col-span-2 布局与端口 ||22 兜底)。

顺手规范化的一致性修正(逐条):
1. vless Reality 指纹:硬编码中文 → i18n FingerprintField
2. vmess TLS 指纹:补回 none 选项
3. anytls TLS 指纹:补 none + i18n placeholder
4. anytls Reality 指纹:补 none + i18n placeholder + desc
5. anytls Reality shortId:补 shortIdDesc
6. anytls port:补 portDesc
7. http port:补 portDesc(动态 443/80 placeholder 经 prop 保留)
8. socks port:补 portDesc
9. anytls/http SNI:字面量 "SNI (...)" → t('servers.sni')+optional

network 下拉因各协议 enum 大小写不同仍内联。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
主线(根因):配置生成层用 parseFloat("1."+minor) 判断版本带,把 minor 当小数
——"1.9"→1.9 被误判 >1.13、"1.20"→1.2 被误判 <1.13。而核心更新层早已用整数编码避坑。
本次把两边统一到单一权威 src/main/utils/version.ts。

1. 版本解析统一(主线)
   - 新增 utils/version.ts:encodeMajorMinor(major*1000+minor,容忍 v 前缀/后缀)+
     coreVersionAtLeast(未知→现代,匹配打包核 ≥1.13.13 与 getCoreVersion 的 1.13.0 兜底)。
   - ProxyManager 6 处版本门(含一处复用 versionNum 的下游站点)改用 coreVersionAtLeast。
   - CoreUpdateService 删私有 encodeMajorMinor,改用共享版(消除双实现)。
   - 真实版本映射完全一致,额外修正 1.9/1.20 误判;6 站点统一未知→现代默认(经
     caller-trace 确认 coreVersion 在生成时恒为真实版本,旧 legacy 兜底为死路径)。

2. ws early-data 落配置:generateTransportConfig 补 emit max_early_data /
   early_data_header_name(订阅/分享链已解析存入 wsSettings,此前未落运行时配置致静默失效)。

3. 下载完整性校验:downloadFile 累计字节与 Content-Length 比对,截断下载经 handleError
   自动换镜像重试一次,拦截不完整包流入 extractCore。

4. naive 漂移提示:核心更新后若存在 naive 节点,提示 naive 依赖随 app 打包的 libcronet、
   跨版本更新可能 ABI 漂移(该漂移运行时 dlopen 才暴露,sing-box check 预检无法发现)。

build:main + eslint 通过;独立 review 全绿(语义保形经 caller-trace 确认)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
独立 review 发现的 P1 项(栈在 P0 之上)。

1. ProxyManager.canHotSwitch:JSON.stringify 深比较改为递归按 key 排序的 stableStringify,
   渲染层重建配置对象键序不同不再误判"配置已变"而退回整重启、废掉热切换。数组顺序保留
   (customRules/appRules 顺序具语义);丢弃 undefined 键,与 JSON.stringify 语义一致。

2. CoreUpdateService.compareVersions:容忍前导 v 与 -prerelease/+build 后缀、每段 NaN→0,
   修 "v1.13.13"/"1.13.13-beta" 这类标签的误比较。

3. 核心更新抑制闩兜底计时器:更新后若迟迟无 'started'/'error' 解决待验证态(如更新时代理
   未运行、用户不重启),PENDING_MAX_AGE_MS 到期清待验证 + 解除自动重启抑制,避免抑制闩
   永久挂起。在成功稳定/回滚路径中清除该计时器(单飞 updateCore 保证不串台)。

4. 订阅 SSRF 兜底:fetchSubscription 限 http(s) 协议、拒指向本机/内网/link-local/CGNAT 的
   字面 IP 主机(拦 file://、169.254.169.254 云元数据、回环/内网)。fc/fd 判定加冒号闸门,
   不误伤 fcm.googleapis.com 等公网主机名。基于主机名的 DNS 旁路仍存在(字面 IP 兜底)。

5. 订阅空结果区分:0 节点时区分"格式不识别"与"节点 URL 都解析失败",不再静默冒充成功。

6. JSON 订阅 ws transport.host 折叠进 Host header(与 httpupgrade 对齐),修同一节点经
   JSON 订阅 vs 分享链解析出不一致 wsSettings(丢 Host)。

build:main + eslint 通过;独立 review 收口(2 处发现——fc/fd 主机名误拦、undefined↔null
误判——已修复并验证)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Sway-Chan

Copy link
Copy Markdown
Collaborator Author

栈在 #48 / #49 / #50 / #51 / #52 / #53 之上,建议最后合并。前序合并后本 PR diff 自动收敛为仅 P1 加固。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants