mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-04 00:53:56 +08:00
🏅 Sync 2026-10-06 17:10:26
This commit is contained in:
1 parent
c3b5349800
commit
4d2ba50e17
35 files changed
+1231
-76
No files matched your search
@@ -290,6 +290,12 @@ echo "== subscription updater actually runs =="
|
||||
# URL points at a closed port, so main() fails before its reload call.
|
||||
sh "$ROOT/tests/ucode/test_subscription_updater_runs.sh" "$ROOT" "$WORK/updater_runs" || FAILED=1
|
||||
|
||||
echo "== udp transport verdict =="
|
||||
# Decides whether QUIC is rejected in the fw4 ruleset. Its 'same' +
|
||||
# TCP-transport-main-node case is the regression: both UCI fields read
|
||||
# perfectly reasonable and the QUIC reject stayed off anyway.
|
||||
sh "$ROOT/tests/ucode/test_udp_transport.sh" "$ROOT" "$WORK/udp_transport" || FAILED=1
|
||||
|
||||
echo "== rpcd method behaviour =="
|
||||
# The rpcd module used to be syntax-checked and never executed, which is how
|
||||
# certificate_write('client_ech_conf') stayed broken from the initial commit:
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Unit tests for udp_transport_verdict() in homeproxy-pro.uc.
|
||||
#
|
||||
# The judgement decides whether QUIC is rejected in the fw4 ruleset, so every
|
||||
# branch is pinned: each of the four main_udp_node values, and each hop of
|
||||
# the resolution ('same' -> the main node, 'urltest' -> every member, a named
|
||||
# section, and 'nil').
|
||||
#
|
||||
# The interesting case is 'same' with a TCP-transport main node - the shape
|
||||
# this function exists for. It is invisible in the UCI value alone: both
|
||||
# fields read perfectly reasonable.
|
||||
#
|
||||
# A real UCI cursor is required (the function reads configuration), so the
|
||||
# runner stages a sandboxed config dir and passes the cursor in. cursor(dir)
|
||||
# reads <dir>/<name> directly, NOT <dir>/config/<name>.
|
||||
#
|
||||
# Each case re-seeds the sandbox from inside the .uc driver through the
|
||||
# cursor's own API and re-reads with a fresh cursor, so libuci's change
|
||||
# tracking cannot carry one case's options into the next.
|
||||
#
|
||||
# Usage: sh tests/ucode/test_udp_transport.sh <repo-root> [work-dir]
|
||||
|
||||
ROOT="${1:-.}"
|
||||
WORK="${2:-/tmp/hp-udp-transport-test}"
|
||||
|
||||
ROOT="$(cd "$ROOT" && pwd)"
|
||||
FAILED=0
|
||||
|
||||
SANDBOX="$WORK/sandbox"
|
||||
STAGE="$WORK/scripts"
|
||||
|
||||
rm -rf "$WORK"
|
||||
mkdir -p "$SANDBOX" "$STAGE"
|
||||
|
||||
if ! command -v ucode > "/dev/null" 2>&1; then
|
||||
echo "NOT RUN: ucode is not on PATH."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# /sbin/validate_data does not exist off-target and production homeproxy-pro.uc
|
||||
# shells out to it from validation(); the driver never calls validation(), but
|
||||
# the module still has to load. tests/ucode/test_mock_sync.sh rewrites the
|
||||
# same literal for the same reason.
|
||||
VALIDATE_DATA="${HP_VALIDATE_DATA:-/sbin/validate_data}"
|
||||
if [ ! -x "$VALIDATE_DATA" ] && [ -x "$ROOT/tests/toolchain/validate-data.sh" ]; then
|
||||
VALIDATE_DATA="$ROOT/tests/toolchain/validate-data.sh"
|
||||
fi
|
||||
|
||||
sed -e "s#/sbin/validate_data#$VALIDATE_DATA#" \
|
||||
"$ROOT/root/etc/homeproxy-pro/scripts/homeproxy-pro.uc" > "$STAGE/homeproxy-pro.uc"
|
||||
cp "$ROOT/tests/ucode/test_udp_transport.uc" "$STAGE/test_udp_transport.uc"
|
||||
|
||||
cd "$STAGE" || exit 1
|
||||
if HP_T_SANDBOX="$SANDBOX" ucode test_udp_transport.uc; then
|
||||
echo "PASS: udp_transport_verdict"
|
||||
else
|
||||
echo "FAIL: udp_transport_verdict"
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
exit "$FAILED"
|
||||
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/ucode
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Driver for tests/ucode/test_udp_transport.sh. Builds a UCI sandbox
|
||||
* in-process and asserts udp_transport_verdict()'s verdict for every branch
|
||||
* of its resolution.
|
||||
*
|
||||
* Each case gets a fresh cursor over the same sandbox directory after a fresh
|
||||
* seed, so libuci's change tracking cannot leak one case's options into the
|
||||
* next - a leaked `main_udp_node` would make a later case pass for the wrong
|
||||
* reason, which is the failure mode this file is most exposed to.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { cursor } from 'uci';
|
||||
import { writefile } from 'fs';
|
||||
import { udp_transport_verdict } from 'homeproxy-pro';
|
||||
|
||||
/* getenv(), not argv: a ucode script has no argv global, and the runner
|
||||
* convention here is HP_T_* environment values (see
|
||||
* test_homeproxy_utils.uc). */
|
||||
const SANDBOX = getenv('HP_T_SANDBOX');
|
||||
if (!SANDBOX || SANDBOX === '') {
|
||||
printf('FAIL: HP_T_SANDBOX is not set; run through test_udp_transport.sh\n');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let failures = 0,
|
||||
checks = 0,
|
||||
/* The case currently running, so a bare assertion name can be traced
|
||||
* back. Without it a failure reads "expected anytls, got nil" and there
|
||||
* are three ways to read that. */
|
||||
current_case = '(none)';
|
||||
|
||||
function expect(name, actual, want) {
|
||||
checks++;
|
||||
if (sprintf('%J', actual) !== sprintf('%J', want)) {
|
||||
printf('FAIL [%s] %s: expected %J, got %J\n', current_case, name, want, actual);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Seed the sandbox by rewriting the UCI file from scratch. `opts` sets
|
||||
* options on the `config` section; `nodes` maps a section name to its type.
|
||||
*
|
||||
* Written as text rather than built with add()/set()/commit() on purpose:
|
||||
* every case reuses the same section names, and add() on a name that already
|
||||
* exists does not reliably give back that name - the first couple of cases
|
||||
* passed and then every later one read the section as missing, which looks
|
||||
* exactly like a bug in the function under test and is not one. Truncating
|
||||
* the file first makes each case independent of the ones before it. */
|
||||
function seed(opts, nodes) {
|
||||
let out = 'config homeproxy-pro \'config\'\n';
|
||||
|
||||
/* A UCI list has to be written as `list`, one line per value. Emitting it
|
||||
* as `option` produces a scalar, uci.get() then hands the function a
|
||||
* string, and iterating a string walks its CHARACTERS - which reads as
|
||||
* "the url-test group has no members" for every group. */
|
||||
for (let k in keys(opts)) {
|
||||
let v = opts[k];
|
||||
if (type(v) === 'array') {
|
||||
for (let item in v)
|
||||
out += sprintf('\tlist %s \'%s\'\n', k, item);
|
||||
} else {
|
||||
out += sprintf('\toption %s \'%s\'\n', k, v);
|
||||
}
|
||||
}
|
||||
|
||||
for (let name in keys(nodes || {})) {
|
||||
out += sprintf('config homeproxy-pro \'%s\'\n', name);
|
||||
out += sprintf('\toption type \'%s\'\n', nodes[name]);
|
||||
out += sprintf('\toption address \'%s.example.com\'\n', name);
|
||||
}
|
||||
|
||||
writefile(SANDBOX + '/homeproxy-pro', out);
|
||||
}
|
||||
|
||||
/* Run one case: seed, open a fresh cursor, judge. */
|
||||
function verdict(name, opts, nodes, check) {
|
||||
current_case = name;
|
||||
seed(opts, nodes);
|
||||
let v = udp_transport_verdict('homeproxy-pro', cursor(SANDBOX));
|
||||
check(v);
|
||||
/* The reason is a log line, so it only has to be present and have to
|
||||
* mention the thing it is explaining - not be worded a particular way,
|
||||
* which would break every time the sentence is improved. */
|
||||
if (!v.reason || v.reason === '')
|
||||
expect(name + '.reason is set', v.reason, 'a non-empty sentence');
|
||||
}
|
||||
|
||||
const HY2 = { 'nodeHy2': 'hysteria2' },
|
||||
ANYTLS = { 'nodeAnyTLS': 'anytls' },
|
||||
MIXED = { 'nodeHy2': 'hysteria2', 'nodeTrojan': 'trojan' };
|
||||
|
||||
/* --- 'same': the case this function exists for ------------------------- */
|
||||
|
||||
verdict('same + udp-native main', { routing_mode: 'bypass_mainland_china',
|
||||
main_node: 'nodeHy2', main_udp_node: 'same' }, HY2,
|
||||
v => {
|
||||
expect('native', v.native, true);
|
||||
expect('configured', v.configured, true);
|
||||
expect('protocol', v.protocol, 'hysteria2');
|
||||
});
|
||||
|
||||
/* The regression: a TCP-transport main node with 'same'. The UCI values look
|
||||
* entirely reasonable, which is exactly why this was missed - the QUIC
|
||||
* reject stayed off and QUIC was tunnelled over TCP. */
|
||||
verdict('same + TCP-transport main', { routing_mode: 'bypass_mainland_china',
|
||||
main_node: 'nodeAnyTLS', main_udp_node: 'same' }, ANYTLS,
|
||||
v => {
|
||||
expect('native', v.native, false);
|
||||
expect('configured', v.configured, true);
|
||||
expect('protocol', v.protocol, 'anytls');
|
||||
});
|
||||
|
||||
verdict('same + no main node', { routing_mode: 'bypass_mainland_china',
|
||||
main_udp_node: 'same' }, {},
|
||||
v => {
|
||||
expect('native', v.native, false);
|
||||
expect('configured', v.configured, false);
|
||||
});
|
||||
|
||||
/* --- a named UDP node --------------------------------------------------- */
|
||||
|
||||
verdict('named udp-native node', { routing_mode: 'bypass_mainland_china',
|
||||
main_node: 'nodeAnyTLS', main_udp_node: 'nodeHy2' }, HY2,
|
||||
v => {
|
||||
expect('native', v.native, true);
|
||||
expect('protocol', v.protocol, 'hysteria2');
|
||||
});
|
||||
|
||||
/* The pairing that actually fixes the measured problem: TCP main, UDP-native
|
||||
* sidecar. */
|
||||
verdict('named UDP-native beside TCP main', { routing_mode: 'bypass_mainland_china',
|
||||
main_node: 'nodeAnyTLS', main_udp_node: 'nodeHy2' }, MIXED,
|
||||
v => {
|
||||
expect('native', v.native, true);
|
||||
expect('protocol', v.protocol, 'hysteria2');
|
||||
});
|
||||
|
||||
verdict('named TCP-transport node', { routing_mode: 'bypass_mainland_china',
|
||||
main_node: 'nodeHy2', main_udp_node: 'nodeAnyTLS' },
|
||||
{ 'nodeHy2': 'hysteria2', 'nodeAnyTLS': 'anytls' },
|
||||
v => {
|
||||
expect('native', v.native, false);
|
||||
expect('protocol', v.protocol, 'anytls');
|
||||
});
|
||||
|
||||
verdict('named node that does not exist', { routing_mode: 'bypass_mainland_china',
|
||||
main_udp_node: 'nodeMissing' }, {},
|
||||
v => {
|
||||
expect('native', v.native, false);
|
||||
expect('configured', v.configured, false);
|
||||
});
|
||||
|
||||
/* --- url-test membership ------------------------------------------------ */
|
||||
|
||||
/* Conservative on purpose: the group picks a member per connection, so ONE
|
||||
* TCP member is enough to put QUIC back on the stalling path whenever it
|
||||
* happens to be the selected one. */
|
||||
verdict('urltest with one TCP member', { routing_mode: 'bypass_mainland_china',
|
||||
main_udp_node: 'urltest',
|
||||
main_udp_urltest_nodes: [ 'nodeHy2', 'nodeTrojan' ] }, MIXED,
|
||||
v => {
|
||||
expect('native', v.native, false);
|
||||
expect('configured', v.configured, true);
|
||||
expect('protocol', v.protocol, 'trojan');
|
||||
});
|
||||
|
||||
verdict('urltest all UDP-native', { routing_mode: 'bypass_mainland_china',
|
||||
main_udp_node: 'urltest',
|
||||
main_udp_urltest_nodes: [ 'nodeHy2', 'nodeHy2' ] }, HY2,
|
||||
v => {
|
||||
expect('native', v.native, true);
|
||||
});
|
||||
|
||||
verdict('urltest with no members', { routing_mode: 'bypass_mainland_china',
|
||||
main_udp_node: 'urltest' }, {},
|
||||
v => {
|
||||
expect('native', v.native, false);
|
||||
expect('configured', v.configured, false);
|
||||
});
|
||||
|
||||
/* --- the remaining shapes ----------------------------------------------- */
|
||||
|
||||
verdict('udp disabled', { routing_mode: 'bypass_mainland_china',
|
||||
main_node: 'nodeHy2', main_udp_node: 'nil' }, HY2,
|
||||
v => {
|
||||
expect('native', v.native, false);
|
||||
expect('configured', v.configured, false);
|
||||
});
|
||||
|
||||
/* An unset main_udp_node is 'nil', not an error: that is the factory default
|
||||
* and a fresh install must not read as misconfigured. */
|
||||
verdict('main_udp_node unset', { routing_mode: 'bypass_mainland_china',
|
||||
main_node: 'nodeHy2' }, HY2,
|
||||
v => {
|
||||
expect('native', v.native, false);
|
||||
expect('configured', v.configured, false);
|
||||
});
|
||||
|
||||
/* Custom mode is the user driving the rule graph by hand; this function must
|
||||
* not second-guess them, which is also what the QUIC gate did before it
|
||||
* learned to read the transport. */
|
||||
verdict('custom routing mode', { routing_mode: 'custom',
|
||||
main_node: 'nodeAnyTLS', main_udp_node: 'same' }, ANYTLS,
|
||||
v => {
|
||||
expect('native', v.native, true);
|
||||
expect('protocol', v.protocol, 'custom');
|
||||
});
|
||||
|
||||
/* --- the set itself ----------------------------------------------------- */
|
||||
|
||||
/* Every protocol the UI offers is classified, not just the ones this change
|
||||
* happens to touch. A protocol added to homeproxy-pro.js's table without a
|
||||
* decision here fails this, which is the point: the set has to be a
|
||||
* deliberate answer rather than a default that silently means "no".
|
||||
*
|
||||
* The native list is duplicated here on purpose - a test that read the
|
||||
* constant under test would agree with any change to it, including a wrong
|
||||
* one. Both lists come from the protocol table in
|
||||
* htdocs/luci-static/resources/homeproxy-pro.js. */
|
||||
const NATIVE = [ 'hysteria', 'hysteria2', 'tuic', 'wireguard' ],
|
||||
NOT_NATIVE = [ 'anytls', 'http', 'mixed', 'naive', 'shadowsocks',
|
||||
'shadowtls', 'snell', 'socks', 'ssh', 'trojan',
|
||||
'vless', 'vmess' ];
|
||||
|
||||
for (let type in NATIVE) {
|
||||
verdict('native: ' + type, { routing_mode: 'bypass_mainland_china',
|
||||
main_udp_node: 'nodeP' }, { 'nodeP': type },
|
||||
v => {
|
||||
expect('native', v.native, true);
|
||||
expect('protocol', v.protocol, type);
|
||||
});
|
||||
}
|
||||
|
||||
for (let type in NOT_NATIVE) {
|
||||
verdict('not native: ' + type, { routing_mode: 'bypass_mainland_china',
|
||||
main_udp_node: 'nodeP' }, { 'nodeP': type },
|
||||
v => {
|
||||
expect('native', v.native, false);
|
||||
expect('configured', v.configured, true);
|
||||
expect('protocol', v.protocol, type);
|
||||
});
|
||||
}
|
||||
|
||||
/* A protocol nobody has classified must read as not-native. Failing closed
|
||||
* here means an unrecognised node gets QUIC rejected - browsers fall back to
|
||||
* TCP, which every transport carries - rather than QUIC tunnelled over a path
|
||||
* that may not be able to carry it. */
|
||||
verdict('unclassified protocol fails closed', { routing_mode: 'bypass_mainland_china',
|
||||
main_udp_node: 'nodeP' }, { 'nodeP': 'something-new' },
|
||||
v => {
|
||||
expect('native', v.native, false);
|
||||
expect('configured', v.configured, true);
|
||||
});
|
||||
|
||||
printf('%d checks, %d failures\n', checks, failures);
|
||||
exit(failures === 0 ? 0 : 1);
|
||||
Reference in new issue
Block a user