✨ Sync 2026-09-19 17:06:32

This commit is contained in:
github-actions[bot] committed 2026-09-19 17:06:32 +08:00
1 parent cee0877435
commit d1cbb03aed
8 files changed
+251 -7

No files matched your search

+2 -2
View File
@@ -22,8 +22,8 @@ LUCI_DEPENDS:= \
+ucode-mod-digest
PKG_NAME:=luci-app-homeproxy
PKG_VERSION:=27.911.1.14
PKG_RELEASE:=16
PKG_VERSION:=27.919.1.14
PKG_RELEASE:=17
define Package/luci-app-homeproxy/conffiles
/etc/config/homeproxy
+13
View File
@@ -86,6 +86,19 @@
| --- | --- | --- |
| 重构和修复错误 | 重构:前端 TLS/传输表单收敛、generator 共享 TLS/transport 构建、`parse_uri` 拆为 13 个协议函数<br>测试:协议单测 153 条、generator 回归、LuCI 表单快照<br>修复:`executeCommand` 清理、dnsmasq 路径告警、fw4 清单单源、启动日志、PEM 校验、`wGET` 失败原因<br>工程:提高中文翻译率 CI、架构核实 | <br>parse_uri 对比零差异、generator JSON 逐字节一致、表单快照逐字段一致<br>顺带修复 3 个既有缺陷:证书上传、fw4 清理回滚、dnsmasq 路径 |
## 稳定性修复(r12)
修掉两个会让功能整体不可用的缺陷,均由 issue 复现确认。
| 领域 | 变更 |
| --- | --- |
| **订阅抓取**(#5) | 抓取前先探测 wget 实现:GNU wget 用 `-nv`(静音进度条但保留失败原因),uclient-fetch / busybox wget 用 `-q`;`--user-agent` / `--timeout` 一并换成三家通用的 `-U` / `-T`。r11 引入的 `-nv` 是 GNU 专有选项,而 OpenWrt / ImmortalWrt 的 `/usr/bin/wget` 默认由 uclient-fetch 提供,参数解析阶段即失败,导致所有订阅更新以 `no valid node found` 结束 |
| **自定义路由**(#3 #4) | 修复 `routing_mode='custom'` 完全无法生成配置:`direct_overrides` 的使用早于声明(ucode 不做变量提升,strict 模式下直接抛未声明变量);本地规则集残留 legacy `download_detour` 字段,被 sing-box 判为 `unknown field` 而拒绝整个配置 |
| **预设规则集** | `geoip-cn` / `geosite-cn` / `geosite-noncn` 改用 SagerNet 上游 raw URL,并经 `main-out` 下载:直连受 DNS 污染时会超时并静默沿用缓存;每天约 250 KB 的代理开销可忽略,节点引导不受影响 |
| **依赖** | Makefile 补上 `+ip-full`、`+kmod-tun` |
> 回归保障:订阅抓取用 stub wget 覆盖 GNU / 非 GNU 两条分支,并在装有 `/bin/uclient-fetch` 的目标上直接执行生成的命令行;自定义路由新增 `custom.uci` fixture(本地规则集 + 直连节点覆盖 + 路由规则)。
## 运行要求
- ImmortalWrt / OpenWrt ≥ 24.10+(apk 或 opkg 均可安装)
@@ -91,10 +91,44 @@ export function getTime(epoch) {
};
/*
* GNU wget and OpenWrt's default /usr/bin/wget do not accept the same flags.
* On OpenWrt/ImmortalWrt that path comes from the uclient-fetch package
* (`ALTERNATIVES:=200:/usr/bin/wget:/bin/uclient-fetch`, `PROVIDES:=wget` in
* package/libs/uclient) and busybox ships no wget applet, so the effective
* wget is uclient-fetch unless wget-ssl happens to be installed on top of it.
* GNU-only flags break the fetch outright: uclient-fetch's getopt rejects
* `-nv` with "unrecognized option: n" before a single request is sent.
*
* The probe below tells the two apart. `-nv` (--no-verbose) is how GNU wget
* mutes the progress meter while still printing the failure reason on stderr;
* `-q` is the only meter switch uclient-fetch and busybox wget share.
*/
export function classifyWgetFlavor(exitcode, stdout) {
if (exitcode !== 0 || !stdout)
return 'compat';
return match(stdout, /GNU Wget/) ? 'gnu' : 'compat';
};
let wget_flavor = null;
export function wgetFlavor() {
if (wget_flavor === null) {
const probe = executeCommand('/usr/bin/wget --version') || {};
wget_flavor = classifyWgetFlavor(probe.exitcode, probe.stdout);
}
return wget_flavor;
};
/*
* Fetch a URL and report both the body and, on failure, the reason. The
* reason is wget's own stderr (whitespace collapsed, length-capped) so the
* caller can tell a DNS failure from a timeout or a TLS handshake error.
* Note that uclient-fetch's -q hides most failure messages, so on such a
* target the reason is often empty and only the exit code is left.
*/
export function wGETVerbose(url, ua) {
if (!url || type(url) !== 'string')
@@ -103,8 +137,10 @@ export function wGETVerbose(url, ua) {
if (!ua)
ua = 'Wget/1.21 (HomeProxy, like v2rayN)';
/* -nv (not -q) so wget still reports *why* a fetch failed on stderr */
const output = executeCommand(`/usr/bin/wget -nv -O- --user-agent ${shellQuote(ua)} --timeout=10 ${shellQuote(url)}`) || {};
/* -U/-T are understood by GNU wget, uclient-fetch and busybox wget alike;
* the long forms --user-agent/--timeout are not, busybox wget in
* particular knows neither of them. */
const output = executeCommand(`/usr/bin/wget ${wgetFlavor() === 'gnu' ? '-nv' : '-q'} -O- -U ${shellQuote(ua)} -T 10 ${shellQuote(url)}`) || {};
if (output.exitcode !== 0) {
let reason = trim(output.stderr || '');
reason = reason ? replace(reason, /\s+/g, ' ') : 'no error output';
+2
View File
@@ -38,6 +38,8 @@ sh tests/ucode/run.sh "$PWD"
| `tests/luci-form-snapshot.js` | Dumps every option (name, kind, title, description, depends, values, datatype/default/…) of the node and server views. Diffs against `tests/snapshots/{node,server}.json`, so a refactor that changes a field or its visibility fails. |
| `tests/ucode/test_homeproxy_utils.uc` | `executeCommand()` return shape, stderr/exit-code capture, binary detection, and a descriptor-leak check (200 calls). |
| `tests/ucode/test_homeproxy_utils_inject.uc` | The failure path of `executeCommand()`: the script stages a copy of `homeproxy.uc` whose `system()` call is replaced by `die()`, then checks that the exception still propagates and that neither descriptor leaks. |
| `tests/ucode/test_wget_flavor.uc` | The `wget --version` probe (`classifyWgetFlavor()`) and the command line `wGETVerbose()` builds from it. `run.sh` points the module at a stub wget and runs the file twice — once answering like GNU wget (expects `-nv`), once like uclient-fetch (expects `-q` and no GNU-only option at all). |
| `tests/ucode/test_wget_verbose_uclient.uc` | Runs `wGETVerbose()` against the target's real `/bin/uclient-fetch` (skipped where it is absent). The request goes to a closed loopback port, so the point is only that uclient-fetch gets past getopt — a GNU-only flag in the command line fails the test. |
| `tests/ucode/test_fw4_names.sh` | Keeps the fw4 chain/set inventory in `scripts/fw4_names.sh` (used by `init.d/homeproxy` to clean up on stop) in sync with the objects declared in `scripts/firewall_post.ut`. |
| `tests/ucode/test_parse_uri.uc` | Per-scheme assertions on the `parse_uri()` result: anytls, http/https, hysteria, hysteria2/hy2, snell, socks(4/4a/5/5h), shadowsocks (SIP002 base64 + plain + plugin, Shadowrocket), trojan (ws/grpc), tuic, vless (reality/ws/http/httpupgrade), vmess (ws/h2/httpupgrade/grpc) and SIP008 objects, plus the rejection paths (unsupported kcp/quic, no QUIC support, invalid port, unknown scheme). |
| `tests/ucode/test_generators.sh` | Runs `generate_client.uc`/`generate_server.uc` against the UCI fixtures in `tests/fixtures/generators/` inside a scratch directory (the `uci` cursor and `HP_DIR`/`RUN_DIR` are redirected) and validates each result with `sing-box check`. |
+62
View File
@@ -81,6 +81,68 @@ else
FAILED=1
fi
echo "== wget flavour selection =="
# A stub /usr/bin/wget whose --version answer comes from the environment, so
# both flavours can be exercised on a single host. The empty-answer/non-zero
# case is uclient-fetch, whose getopt rejects --version just like it rejects
# -nv before sending anything.
rm -rf "$WORK/wget_flavor"
mkdir -p "$WORK/wget_flavor"
cat > "$WORK/wget_flavor/wget-stub" <<'STUB'
#!/bin/sh
if [ "$1" = "--version" ]; then
printf '%s\n' "$HP_WGET_STUB_VERSION"
exit "${HP_WGET_STUB_VERSION_RC:-0}"
fi
for arg in "$@"; do printf '%s\n' "$arg"; done >> "$HP_WGET_STUB_LOG"
printf 'stub body\n'
STUB
chmod +x "$WORK/wget_flavor/wget-stub"
sed "s|/usr/bin/wget|$WORK/wget_flavor/wget-stub|" \
"$ROOT/root/etc/homeproxy/scripts/homeproxy.uc" > "$WORK/wget_flavor/homeproxy.uc"
cp "$ROOT/tests/ucode/test_wget_flavor.uc" "$WORK/wget_flavor/"
for flavor in gnu compat; do
if [ "$flavor" = "gnu" ]; then
version="GNU Wget 1.25.0 built on linux-gnu."
rc=0
else
version=""
rc=1
fi
rm -f "$WORK/wget_flavor/args.log"
if ( cd "$WORK/wget_flavor" &&
HP_WGET_EXPECT_FLAVOR="$flavor" \
HP_WGET_STUB_VERSION="$version" \
HP_WGET_STUB_VERSION_RC="$rc" \
HP_WGET_STUB_LOG="$WORK/wget_flavor/args.log" \
ucode test_wget_flavor.uc ); then
echo "PASS: wget flavour selection ($flavor)"
else
echo "FAIL: wget flavour selection ($flavor)"
FAILED=1
fi
done
echo "== real uclient-fetch compatibility =="
# uclient-fetch is the default /usr/bin/wget on OpenWrt/ImmortalWrt, so a
# GNU-only option anywhere in the generated command line has to fail here.
if [ -x /bin/uclient-fetch ]; then
rm -rf "$WORK/uclient"
mkdir -p "$WORK/uclient"
sed "s|/usr/bin/wget|/bin/uclient-fetch|" \
"$ROOT/root/etc/homeproxy/scripts/homeproxy.uc" > "$WORK/uclient/homeproxy.uc"
cp "$ROOT/tests/ucode/test_wget_verbose_uclient.uc" "$WORK/uclient/"
if ( cd "$WORK/uclient" && ucode test_wget_verbose_uclient.uc ); then
echo "PASS: uclient-fetch accepts the generated command line"
else
echo "FAIL: uclient-fetch rejects the generated command line"
FAILED=1
fi
else
echo "SKIP: /bin/uclient-fetch not installed on this host"
fi
echo "== generator regression tests =="
sh "$ROOT/tests/ucode/test_generators.sh" "$ROOT" "$WORK/generators" || FAILED=1
@@ -0,0 +1,89 @@
#!/usr/bin/ucode
/*
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2026 ImmortalWrt.org
*
* Regression tests for the wget flavour probe and for the command line
* wGETVerbose() builds from it.
*
* OpenWrt/ImmortalWrt's default /usr/bin/wget is uclient-fetch, which rejects
* GNU-only flags such as -nv with "unrecognized option: n" *before* it opens a
* connection, so a single GNU-ism turns every subscription update into
* "no valid node found". run.sh stages a copy of homeproxy.uc whose
* /usr/bin/wget points at a stub; HP_WGET_STUB_VERSION steers the answer of
* the `--version` probe (empty output plus a non-zero exit mimics
* uclient-fetch, whose getopt rejects the option and returns 1).
*/
'use strict';
import { readfile } from 'fs';
import { classifyWgetFlavor, wGETVerbose, wgetFlavor } from 'homeproxy';
let failures = 0, checks = 0;
function expect(name, actual, want) {
checks++;
if (sprintf('%J', actual) !== sprintf('%J', want)) {
printf('FAIL %s: expected %J, got %J\n', name, want, actual);
failures++;
}
}
function has(list, value) {
for (let v in list)
if (v === value)
return true;
return false;
}
/* The value following the first occurrence of `key` */
function after(list, key) {
let prev = null;
for (let v in list) {
if (prev === key)
return v;
prev = v;
}
return null;
}
/* Classification of the `wget --version` probe. */
expect('gnu.version', classifyWgetFlavor(0, 'GNU Wget 1.25.0 built on linux-gnu.\n'), 'gnu');
expect('gnu.multiline', classifyWgetFlavor(0, "GNU Wget 1.21.4 built on linux-gnu.\n\nCopyright (C) 2015 Free Software Foundation.\n"), 'gnu');
expect('uclient.rejects-version', classifyWgetFlavor(1, null), 'compat');
expect('busybox.version', classifyWgetFlavor(0, 'BusyBox v1.36.1 (2024-01-01) multi-call binary.\n'), 'compat');
expect('empty.stdout', classifyWgetFlavor(0, ''), 'compat');
expect('missing.binary', classifyWgetFlavor(127, null), 'compat');
/* wgetFlavor() probes the stub and caches the answer. */
const want_flavor = getenv('HP_WGET_EXPECT_FLAVOR');
expect('flavor', wgetFlavor(), want_flavor);
expect('flavor.cached', wgetFlavor(), want_flavor);
const res = wGETVerbose('https://example.invalid/subs?token=abc', 'HomeProxy test UA');
expect('content', res.content, 'stub body');
expect('error', res.error, null);
const log = readfile(getenv('HP_WGET_STUB_LOG'));
const args = log ? split(trim(log), '\n') : [];
expect('agent flag', has(args, '-U'), true);
expect('agent value', after(args, '-U'), 'HomeProxy test UA');
expect('timeout flag', after(args, '-T'), '10');
expect('output flag', has(args, '-O-'), true);
expect('url', has(args, 'https://example.invalid/subs?token=abc'), true);
/* -nv is GNU wget's --no-verbose; every other wget on the target rejects it,
* and -q is the meter switch they all share. */
expect('meter flag', has(args, want_flavor === 'gnu' ? '-nv' : '-q'), true);
expect('no gnu-only flag', has(args, '-nv'), want_flavor === 'gnu');
expect('no long options', has(args, '--user-agent') || has(args, '--timeout=10'), false);
printf('%d checks, %d failures\n', checks, failures);
exit(failures === 0 ? 0 : 1);
@@ -0,0 +1,42 @@
#!/usr/bin/ucode
/*
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2026 ImmortalWrt.org
*
* Feeds the command line wGETVerbose() builds to the target's real
* uclient-fetch - the default /usr/bin/wget on OpenWrt/ImmortalWrt. run.sh
* rewrites the module's wget path to /bin/uclient-fetch and skips this file
* where that binary does not exist.
*
* A closed loopback port is the entire request: what matters is that
* uclient-fetch gets past getopt. Before the flavour probe existed it did not -
* `-nv` made it print "unrecognized option: n" and exit before the request was
* sent, so every subscription fetch came back empty. Any failure observed here
* must therefore come from the network, never from the option parser.
*/
'use strict';
import { wGETVerbose, wgetFlavor } from 'homeproxy';
let failures = 0, checks = 0;
function expect(name, actual, want) {
checks++;
if (sprintf('%J', actual) !== sprintf('%J', want)) {
printf('FAIL %s: expected %J, got %J\n', name, want, actual);
failures++;
}
}
/* uclient-fetch has no --version and rejects it, so it is classified compat. */
expect('flavor', wgetFlavor(), 'compat');
const res = wGETVerbose('http://127.0.0.1:1/subs', 'HomeProxy test UA');
expect('no content', res.content, null);
expect('not an option error', match(res.error || '', /unrecognized option/) == null, true);
expect('error carries the exit code', match(res.error || '', /^wget exited with status \d+: /) != null, true);
printf('%d checks, %d failures\n', checks, failures);
exit(failures === 0 ? 0 : 1);
+3 -3
View File
@@ -49,12 +49,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=wwand
PKG_VERSION:=1.6.7_p17
PKG_RELEASE:=45
PKG_VERSION:=1.6.7_p21
PKG_RELEASE:=46
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
PKG_SOURCE_VERSION:=245058186b1ee22b8e6eb92ca9745d4ea811b116
PKG_SOURCE_VERSION:=910e38b7e4c92e273b91aeead3bea7e2f7104a27
PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-2.0-only