diff --git a/luci-app-homeproxy-pro/Makefile b/luci-app-homeproxy-pro/Makefile index 54de3d01f..049d7a807 100644 --- a/luci-app-homeproxy-pro/Makefile +++ b/luci-app-homeproxy-pro/Makefile @@ -23,7 +23,7 @@ LUCI_DEPENDS:= \ PKG_NAME:=luci-app-homeproxy-pro PKG_VERSION:=28.9.1.14 -PKG_RELEASE:=4 +PKG_RELEASE:=5 # Only files the package actually ships belong here. The previous list named # four paths that exist nowhere in the tree - certs/ and ruleset/ are created at diff --git a/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/generator/route.uc b/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/generator/route.uc index bb6ff415a..1b3b790ac 100644 --- a/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/generator/route.uc +++ b/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/generator/route.uc @@ -193,7 +193,7 @@ function build_route_proxy(config, dm, ctx, direct_overrides) { type: 'remote', tag: 'geoip-cn', format: 'binary', - url: 'https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs', + url: 'https://gh-proxy.com/raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs', update_interval: '24h', download_detour: 'main-out' }); @@ -201,7 +201,7 @@ function build_route_proxy(config, dm, ctx, direct_overrides) { type: 'remote', tag: 'geosite-cn', format: 'binary', - url: 'https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs', + url: 'https://gh-proxy.com/raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs', update_interval: '24h', download_detour: 'main-out' }); diff --git a/luci-app-homeproxy/Makefile b/luci-app-homeproxy/Makefile index 2c4c555d7..a710a90f3 100644 --- a/luci-app-homeproxy/Makefile +++ b/luci-app-homeproxy/Makefile @@ -23,7 +23,7 @@ LUCI_DEPENDS:= \ PKG_NAME:=luci-app-homeproxy PKG_VERSION:=27.919.1.14 -PKG_RELEASE:=19 +PKG_RELEASE:=20 define Package/luci-app-homeproxy/conffiles /etc/config/homeproxy diff --git a/luci-app-homeproxy/README.md b/luci-app-homeproxy/README.md index a15979b2a..8c2eec1ab 100644 --- a/luci-app-homeproxy/README.md +++ b/luci-app-homeproxy/README.md @@ -7,7 +7,7 @@ **The modern ImmortalWrt proxy platform for ARM64 / AMD64** -基于 sing-box 1.14 内核的现代代理平台 — 简洁、高效、开箱即用。 +本项目发版到R13后将不再频繁维护,除非存在重大修复。日常只推进“luci-app-homeproxy-pro”版本 --- diff --git a/luci-app-homeproxy/root/etc/homeproxy/scripts/generate_client.uc b/luci-app-homeproxy/root/etc/homeproxy/scripts/generate_client.uc index 61ee004f6..0f7a800f0 100755 --- a/luci-app-homeproxy/root/etc/homeproxy/scripts/generate_client.uc +++ b/luci-app-homeproxy/root/etc/homeproxy/scripts/generate_client.uc @@ -1070,12 +1070,12 @@ if (!isEmpty(main_node)) { { tag: 'geoip-cn', path: HP_DIR + '/resources/geoip_cn.srs', - url: 'https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs' + url: 'https://gh-proxy.com/raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs' }, { tag: 'geosite-cn', path: HP_DIR + '/resources/geosite_cn.srs', - url: 'https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs' + url: 'https://gh-proxy.com/raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs' } ]; diff --git a/luci-app-substore/CHANGELOG.md b/luci-app-substore/CHANGELOG.md index daeb85924..042a33ae7 100644 --- a/luci-app-substore/CHANGELOG.md +++ b/luci-app-substore/CHANGELOG.md @@ -2,6 +2,65 @@ All notable changes to this project will be documented in this file. +## [2.5.0-r1] - 订阅可靠性、错误反馈与 WireGuard 去重修复 + +- **修复严重缺陷(订阅更新可能「看起来成功其实失败」)**:`core.sync` 失败时是 + 「返回 `nil, err`」而**不是**抛异常,而调用方只判断了 `pcall` 的第一层返回值。 + `pcall` 返回 `true` 只说明没有抛错,第二个返回值才是 `sync` 自身的结果—— + 于是「同步返回 `nil, err`」被统计成**成功**。 + - `root/usr/bin/substore-cron.sh`:改为 `local ok, res, err = pcall(core.sync, id)`, + 并要求 `ok and res` 才算成功;失败时打印真实原因。同时把脚本输出同时写 stdout 与 + syslog(原先只进 syslog,cron 邮件里看不到) + - `root/usr/lib/lua/luci/controller/admin/substore.lua`:`action_update` 同样处理两层结果 +- **修复严重缺陷(代理静默失效,属 silent fallback)**: + - `core.sync` 原先在「代理已启用但代理地址无效」时只记一条日志就**直连下载**, + 用户会以为流量走了代理、实际暴露真实 IP。现改为**明确失败**并把原因写入订阅状态 + - wget 后端原先在遇到它不支持的代理协议(`socks*`)时**丢掉代理继续直连**。 + 现改为明确报错并提示安装 curl 或改用 http 代理(busybox wget 只能通过 + `http_proxy` / `https_proxy` 环境变量使用 http(s) 代理) +- **修复严重缺陷(wget 后端重定向绕过 SSRF 预检)**:curl 路径逐跳校验重定向目标, + wget 路径**完全不校验**,可被重定向到 `127.0.0.1` / 内网而绕过 `check_public`。 + 现 wget 改用 `-S` 输出响应头日志,由新增的 `http.validate_redirect_chain` + 按出现顺序解析整条重定向链并逐跳复检,任一跳不安全即整体失败并**丢弃响应体**。 + 另修复协议相对地址(`//host/path`)被误当成同主机路径而漏检的问题 +- **修复严重缺陷(界面失败无反馈)**:订阅的新建 / 保存 / 本地订阅新建 / 保存 + 失败时,控制器只静默跳回列表页,用户看到的是「什么都没发生」,与成功无法区分。 + 现失败原因经 `?err=` 回传到列表页并渲染为可见的错误提示 +- **修复严重缺陷(WireGuard 节点被错误去重合并)**:去重键为 `proto+server+port`, + 而同一 endpoint 上的**不同 peer 公钥是不同节点**,会被错误合并而丢节点。 + 现 WireGuard / `wg` 节点的去重键并入 peer 公钥(兼容 `public-key` / `public_key` / + `peer-public-key` / `peer_public_key` 四种写法);其余协议去重行为**完全不变** +- **修复严重缺陷(多个 WireGuard 节点被拼进一个 `.conf`)**:一个 `.conf` 文件只能包含 + 一个 `[Interface]`,拼接会产生客户端无法导入(或只取首段)的畸形配置。 + 现多于一个 WireGuard 节点时**明确报错**(错误信息含节点数与「一条隧道」提示), + 而不是静默拼接 +- **修复 AmneziaWG `.conf` 导出键名靠猜测**:原先用「首字母大写」由内部键名推导 `.conf` + 键名,多词键会被写错(`header-protection-key` → `Header-protection-key`, + 正确为 `HeaderProtectionKey`),未知键可能让客户端拒绝整份配置。 + 现改为显式映射表,只输出能**确定**键名的键(`Jc/Jmin/Jmax/S1–S4/H1–H4/I1–I5/J1–J3/Itime`), + 映射不到的键一律不输出;已支持键集合零变化 +- **修复模板注入(HTML / JS)**:7 个模板把用户可控数据(订阅名、URL、代理、节点字段、 + 解析错误信息)直接以 `<%= %>` 原样输出。现按上下文分别转义: + HTML 用 `luci.util.pcdata`,注入 JS 的字符串用 `json_encode`(并转义 `` 防止提前 + 闭合 `script`),URL 用 `urlencode` +- **修复日志/错误信息泄漏凭据**:新增 `http.redact_proxy`(代理地址去凭据)与 + `http.scrub_credentials`(抹掉 URL 中的 `//user:pass@`),用于日志与订阅错误信息 +- **修复失败路径污染 `node_count`**:同步失败时不再把 `node_count` 清零—— + 磁盘上的旧节点仍在、订阅链接仍在下发,清零会让界面显示与实际不符 +- 新增测试:`tests/controller_local_test.lua`(22 项)、`tests/template_escape_test.lua`(21 项)、 + `tests/cron_result_test.sh`(5 项);`tests/wireguard_conf_test.lua` 扩至 93 项、 + `tests/node_extended_test.lua` 扩至 28 项、`tests/http_proxy_test.lua` 扩充代理矩阵与 + 重定向链复检。全部测试共 1174 项断言通过 +- 版本号 2.4.0-r2 → 2.5.0-r1;README.md / README.en.md / docs/INSTALL.md 同步 +- 已知限制(本次**未**修改,另行处理):`check_public` 在无 DNS 解析能力时放行 + (fail-open)且解析与下载之间存在 TOCTOU 窗口;wget 路径的重定向校验是事后的 + (busybox wget 无 `--max-redirect`),响应体虽被丢弃但请求已经发出,且无法限制下载体积; + AmneziaWG 3.0 / 3.1 新增的 9 个字段(`HeaderProtectionKey`、`ContentPaddingAddition`、 + `RekeyAfterTime`、`RekeyTimeout`、`RejectAfterTime`、`KeepaliveTimeout`、 + `MaxHandshakeAttempts`、`RandomTrailers`、`DisableCookies`)**不被 `.conf` 解析器接受** + (白名单外,导入即丢弃),但会从 Clash / JSON / `wireguard://` 导入路径原样透传; + sing-box 本身不支持 AmneziaWG,其输出会丢弃 AWG 参数 + ## [2.4.0-r2] - 修复 vmess 加密方式与 TLS 层混淆 - **修复严重缺陷**:经典 vmess 分享链接 `vmess://base64(json)` 的 `scy` 是**加密方式** diff --git a/luci-app-substore/Makefile b/luci-app-substore/Makefile index c92ca59a4..c9f6a3abb 100644 --- a/luci-app-substore/Makefile +++ b/luci-app-substore/Makefile @@ -9,8 +9,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-substore # 版本约定:每次提交 PKG_RELEASE +1(2.1.2-r1 ~ r9); # 达到 r10 时 PKG_VERSION 末位 +1(2.1.2 -> 2.1.3),PKG_RELEASE 重置为 1。 -PKG_VERSION:=2.4.0 -PKG_RELEASE:=8 +PKG_VERSION:=2.5.0 +PKG_RELEASE:=9 LUCI_DEPENDS:=+luci-lua-runtime +luci-compat diff --git a/luci-app-substore/README.en.md b/luci-app-substore/README.en.md index 29662f107..a4ea8ce72 100644 --- a/luci-app-substore/README.en.md +++ b/luci-app-substore/README.en.md @@ -93,18 +93,18 @@ group them, then re-emit them in a format your client can consume. ## Installation > The version in the package name must match `PKG_VERSION` / `PKG_RELEASE` in the -> [Makefile](Makefile) (currently `2.4.0-r2`). +> [Makefile](Makefile) (currently `2.5.0-r1`). opkg (OpenWrt / ImmortalWrt 24.10 and earlier): ```bash -opkg install luci-app-substore-2.4.0-r2.ipk +opkg install luci-app-substore-2.5.0-r1.ipk ``` apk (OpenWrt / ImmortalWrt 25.12+): ```bash -apk add --allow-untrusted luci-app-substore-2.4.0-r2.apk +apk add --allow-untrusted luci-app-substore-2.5.0-r1.apk ``` Then open LuCI: **Services → Subscriptions**. diff --git a/luci-app-substore/README.md b/luci-app-substore/README.md index 9283ada9d..a63b8f494 100644 --- a/luci-app-substore/README.md +++ b/luci-app-substore/README.md @@ -80,18 +80,18 @@ ## 安装 > 包名中的版本号必须与 [Makefile](Makefile) 的 `PKG_VERSION` / `PKG_RELEASE` 保持一致 -> (当前 `2.4.0-r2`)。 +> (当前 `2.5.0-r1`)。 opkg(OpenWrt / ImmortalWrt 24.10 及更早): ```bash -opkg install luci-app-substore-2.4.0-r2.ipk +opkg install luci-app-substore-2.5.0-r1.ipk ``` apk(OpenWrt / ImmortalWrt 25.12+): ```bash -apk add --allow-untrusted luci-app-substore-2.4.0-r2.apk +apk add --allow-untrusted luci-app-substore-2.5.0-r1.apk ``` 然后在 LuCI 菜单打开:**服务 → 订阅**。 diff --git a/luci-app-substore/docs/INSTALL.md b/luci-app-substore/docs/INSTALL.md index 2d0c179d7..39f22dfde 100644 --- a/luci-app-substore/docs/INSTALL.md +++ b/luci-app-substore/docs/INSTALL.md @@ -3,18 +3,18 @@ > NOTE: stage-0 skeleton has not yet been installed/verified on a target device. > This document will be updated once installation is validated. > -> 包名中的版本号需与 Makefile 的 `PKG_VERSION` / `PKG_RELEASE` 保持同步(当前 2.4.0-r2)。 +> 包名中的版本号需与 Makefile 的 `PKG_VERSION` / `PKG_RELEASE` 保持同步(当前 2.5.0-r1)。 ## Install (opkg — OpenWrt / ImmortalWrt 24.10 及更早) ```bash -opkg install luci-app-substore-2.4.0-r2.ipk +opkg install luci-app-substore-2.5.0-r1.ipk ``` ## Install (apk — OpenWrt / ImmortalWrt 25.12+) ```bash -apk add --allow-untrusted luci-app-substore-2.4.0-r2.apk +apk add --allow-untrusted luci-app-substore-2.5.0-r1.apk ``` Refresh LuCI: diff --git a/luci-app-substore/root/usr/bin/substore-cron.sh b/luci-app-substore/root/usr/bin/substore-cron.sh index 51ef9be0e..5e6c85ae5 100755 --- a/luci-app-substore/root/usr/bin/substore-cron.sh +++ b/luci-app-substore/root/usr/bin/substore-cron.sh @@ -3,7 +3,8 @@ # 用法:substore-cron.sh [订阅ID](不传则更新全部已启用订阅) # 注意:显式设置 package.path,确保独立 cron 进程能找到 /usr/lib/lua 下的模块 -DATA_DIR="/etc/substore" +# 数据目录可被环境变量覆盖,仅用于测试;生产环境仍为 /etc/substore +DATA_DIR="${SUBSTORE_DATA_DIR:-/etc/substore}" LIST_FILE="$DATA_DIR/subscriptions.json" SUB_ID="${1:-}" LUA_BIN="" @@ -23,19 +24,24 @@ fi [ -f "$LIST_FILE" ] || exit 0 export SUB_ID +OUT=$( LUA_PATH="/usr/lib/lua/?.lua;/usr/share/lua/?.lua;${LUA_PATH:-}" \ "$LUA_BIN" -e ' local core = require("substore.core") local sub_id = os.getenv("SUB_ID") local ok_count, fail_count = 0, 0 +-- core.sync 失败时是「返回 nil, err」而非抛异常,因此 pcall 必须处理两层结果: +-- pcall 返回 true 只说明没有抛错,第二个返回值才是 sync 自身的结果。 +-- 只判断第一层会把「sync 返回 nil, err」统计成成功。 local function sync_one(id) - local ok, err = pcall(function() return core.sync(id) end) - if ok then + local ok, res, err = pcall(core.sync, id) + if ok and res then ok_count = ok_count + 1 else fail_count = fail_count + 1 - io.stderr:write("Failed sync " .. tostring(id) .. ": " .. tostring(err) .. "\n") + local reason = ok and err or res + io.stderr:write("Failed sync " .. tostring(id) .. ": " .. tostring(reason) .. "\n") end end @@ -47,4 +53,12 @@ else end end io.stdout:write(string.format("substore cron done: %d ok, %d failed\n", ok_count, fail_count)) -' 2>&1 | logger -t luci-app-substore +' 2>&1 +) + +# 输出到 stdout(cron 会按需邮件/记录),同时写 syslog +printf '%s\n' "$OUT" +if command -v logger >/dev/null 2>&1; then + printf '%s\n' "$OUT" | logger -t luci-app-substore +fi +exit 0 diff --git a/luci-app-substore/root/usr/lib/lua/luci/controller/admin/substore.lua b/luci-app-substore/root/usr/lib/lua/luci/controller/admin/substore.lua index f1cbd9d84..00a889c9d 100644 --- a/luci-app-substore/root/usr/lib/lua/luci/controller/admin/substore.lua +++ b/luci-app-substore/root/usr/lib/lua/luci/controller/admin/substore.lua @@ -2,9 +2,15 @@ module("luci.controller.admin.substore", package.seeall) -local function back_to_list() +-- 返回列表页。err 非空时把错误带到列表页显示(§18:失败必须让用户看见, +-- 不能「失败了却看起来像成功」)。沿用 LuCI 既有的 query + 模板渲染,不引入新 framework。 +local function back_to_list(err) local http = require("luci.http") - http.redirect(luci.dispatcher.build_url("admin", "services", "substore", "list")) + local url = luci.dispatcher.build_url("admin", "services", "substore", "list") + if err ~= nil and tostring(err) ~= "" then + url = url .. "?err=" .. luci.util.urlencode(tostring(err)) + end + http.redirect(url) end function index() @@ -84,18 +90,20 @@ function action_create() local proxy_enable = http.formvalue("proxy_enable") or "0" if proxy_enable ~= "1" then proxy_enable = "0" end local proxy = (http.formvalue("proxy") or ""):gsub("^%s+", ""):gsub("%s+$", "") - if name ~= "" and url ~= "" then - local cron_enable, cron_time = read_cron_fields() - local rules = read_rules_fields() - core.add(name, url, { - proxy_enable = proxy_enable, proxy = proxy, - cron_enable = cron_enable, cron_time = cron_time, - rules_enable = rules.rules_enable, proto_filter = rules.proto_filter, - keyword_include = rules.keyword_include, keyword_exclude = rules.keyword_exclude, - dedup = rules.dedup, rename_map = rules.rename_map, - }) - core.write_cron() + if name == "" or url == "" then + return back_to_list("名称和 URL 不能为空") end + local cron_enable, cron_time = read_cron_fields() + local rules = read_rules_fields() + local id, err = core.add(name, url, { + proxy_enable = proxy_enable, proxy = proxy, + cron_enable = cron_enable, cron_time = cron_time, + rules_enable = rules.rules_enable, proto_filter = rules.proto_filter, + keyword_include = rules.keyword_include, keyword_exclude = rules.keyword_exclude, + dedup = rules.dedup, rename_map = rules.rename_map, + }) + if not id then return back_to_list(err or "创建订阅失败") end + core.write_cron() end back_to_list() end @@ -110,18 +118,21 @@ function action_save() local proxy_enable = http.formvalue("proxy_enable") or "0" if proxy_enable ~= "1" then proxy_enable = "0" end local proxy = (http.formvalue("proxy") or ""):gsub("^%s+", ""):gsub("%s+$", "") - if name ~= "" and url ~= "" then - local cron_enable, cron_time = read_cron_fields() - local rules = read_rules_fields() - core.save_meta(id, { - name = name, url = url, proxy_enable = proxy_enable, proxy = proxy, - cron_enable = cron_enable, cron_time = cron_time, - rules_enable = rules.rules_enable, proto_filter = rules.proto_filter, - keyword_include = rules.keyword_include, keyword_exclude = rules.keyword_exclude, - dedup = rules.dedup, rename_map = rules.rename_map, - }) - core.write_cron() + if id == "" then return back_to_list("缺少订阅 ID") end + if name == "" or url == "" then + return back_to_list("名称和 URL 不能为空") end + local cron_enable, cron_time = read_cron_fields() + local rules = read_rules_fields() + local ok, err = core.save_meta(id, { + name = name, url = url, proxy_enable = proxy_enable, proxy = proxy, + cron_enable = cron_enable, cron_time = cron_time, + rules_enable = rules.rules_enable, proto_filter = rules.proto_filter, + keyword_include = rules.keyword_include, keyword_exclude = rules.keyword_exclude, + dedup = rules.dedup, rename_map = rules.rename_map, + }) + if not ok then return back_to_list(err or "保存失败") end + core.write_cron() end back_to_list() end @@ -134,15 +145,17 @@ function action_local_create() local content = http.formvalue("content") or "" local local_mode = http.formvalue("local_mode") or "text" local rules = read_rules_fields() - if name ~= "" and content ~= "" then - core.add_local(name, content, local_mode, { - rules_enable = rules.rules_enable, - proto_filter = rules.proto_filter, - keyword_include = rules.keyword_include, - keyword_exclude = rules.keyword_exclude, - dedup = rules.dedup, - }) - end + -- §19:空名称 / 空内容必须明确报错,不能无声创建无效订阅 + if name == "" then return back_to_list("名称不能为空") end + if content == "" then return back_to_list("订阅内容不能为空") end + local id, err = core.add_local(name, content, local_mode, { + rules_enable = rules.rules_enable, + proto_filter = rules.proto_filter, + keyword_include = rules.keyword_include, + keyword_exclude = rules.keyword_exclude, + dedup = rules.dedup, + }) + if not id then return back_to_list(err or "创建本地订阅失败") end end back_to_list() end @@ -156,19 +169,23 @@ function action_local_save() local content = http.formvalue("content") or "" local local_mode = http.formvalue("local_mode") or "text" local rules = read_rules_fields() - if id ~= "" and name ~= "" and content ~= "" then - core.save_meta(id, { - name = name, - raw_content = content, - local_mode = local_mode, - rules_enable = rules.rules_enable, - proto_filter = rules.proto_filter, - keyword_include = rules.keyword_include, - keyword_exclude = rules.keyword_exclude, - dedup = rules.dedup, - }) - core.sync(id) - end + if id == "" then return back_to_list("缺少订阅 ID") end + if name == "" then return back_to_list("名称不能为空") end + if content == "" then return back_to_list("订阅内容不能为空") end + local ok, err = core.save_meta(id, { + name = name, + raw_content = content, + local_mode = local_mode, + rules_enable = rules.rules_enable, + proto_filter = rules.proto_filter, + keyword_include = rules.keyword_include, + keyword_exclude = rules.keyword_exclude, + dedup = rules.dedup, + }) + if not ok then return back_to_list(err or "保存失败") end + -- 解析失败会写进 meta.error(列表页 Status 列可见),此处再明确提示一次 + local sok, serr = core.sync(id) + if not sok then return back_to_list(serr or "解析订阅内容失败") end end back_to_list() end @@ -315,12 +332,15 @@ function action_update() -- local subscription does not support auto update return end - -- pcall fallback: parse or write error will not 500 the page, error saved to list status - local ok, err = pcall(function() - return core.sync(id) - end) + -- pcall 只保证「不抛异常」;core.sync 的失败是「返回 nil, err」而非抛错, + -- 因此必须同时检查两层结果,否则失败永远不会写回列表状态。 + local ok, res, err = pcall(core.sync, id) if not ok then - core.save_meta(id, { error = tostring(err), last_update = os.time() }) + -- 抛异常:第二个返回值是错误信息 + core.save_meta(id, { error = tostring(res), last_update = os.time() }) + elseif not res then + -- 正常返回但失败:第三个返回值是错误信息 + core.save_meta(id, { error = tostring(err or "更新失败"), last_update = os.time() }) end end back_to_list() diff --git a/luci-app-substore/root/usr/lib/lua/luci/view/substore/combo.htm b/luci-app-substore/root/usr/lib/lua/luci/view/substore/combo.htm index eee34b3e6..d89612a0c 100644 --- a/luci-app-substore/root/usr/lib/lua/luci/view/substore/combo.htm +++ b/luci-app-substore/root/usr/lib/lua/luci/view/substore/combo.htm @@ -27,7 +27,7 @@