mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-04 07:53:56 +08:00
🤞 Sync 2026-09-30 09:28:31
This commit is contained in:
1 parent
0108cfa10a
commit
3d9edb1c37
78 files changed
+4594
-1815
No files matched your search
@@ -269,6 +269,7 @@ Two further host differences are bridged so the remaining checks still run:
|
||||
| `tests/i18n-coverage.py` | Reports how many `po/templates/homeproxy-pro.pot` strings have a non-fuzzy, non-empty `po/zh_Hans/homeproxy-pro.po` translation. Technical tokens that stay as-is are listed in `tests/i18n-ignore.txt`; anything else missing fails the run (`--fail-below 100`) and adds a job-summary entry. The release workflow (`build.yml`) deliberately warns instead of failing, so a translation gap cannot block packaging - the PR gates already catch it. |
|
||||
| `tests/luci-form-snapshot.js` | Dumps every option (name, kind, title, description, depends, values, datatype/default/…) of the node, client and server views. Diffs against `tests/snapshots/{node,client,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_china_ip_ruleset.sh` | `runtime/china_ip_ruleset.uc`, the generator that turns `china_ip4.txt` into the `type: local` rule-set the route side matches mainland destinations with — the same list `firewall_post.ut` renders `homeproxy_mainland_addr_v4` from, so both sides decide from one source. The cases are the ones that break a router silently: a malformed line must be skipped (sing-box rejects the *whole* rule-set over one bad entry, and the client then will not start), the file must be replaced by rename (the running instance watches it with fswatch), and an empty or unreadable source must fail instead of installing a rule-set that matches nothing. Pins the bundled list's `8.152.0.0/13` — the range `geoip-cn.srs` does not carry, and the reason this exists. |
|
||||
| `tests/ucode/test_resource_blob_sha.sh` | `resource_blob_sha.uc`, the digest the resource updater verifies downloads with. The expected values are git's own (`git hash-object` on the same bytes), covering the two cases an implementation gets wrong quietly: the empty file (a real blob id, not "no digest") and content whose byte length differs from its character count. Unreadable input must produce *no* digest and exit non-zero - the updater treats an empty answer as "cannot verify" and refuses, so a made-up digest would turn an unreadable file into a passing check. Re-checks the repository's real resource files against git when the host has it. |
|
||||
| `tests/ucode/test_homeproxy_utils_inject.uc` | The failure path of `executeCommand()`: the script stages a copy of `homeproxy-pro.uc` whose `system()` call is replaced by `die()`, then checks that the exception still propagates and that neither descriptor leaks. |
|
||||
| `tests/ucode/test_tls_transport.uc` | Direct calls into `buildTLSObject()` / `buildTransportObject()`: client-vs-server-only fields (no `insecure` on a server inbound, no `utls` on a server, `public_key` only on the client and `private_key` only on the server for reality), the ECH client/server split, and the `validateHomeProxyPath()` gate on `cert_path` / `key_path`. This is the boundary the generator fixtures only exercised end-to-end, so it is where the "server emitted insecure=true" and "reality key on the wrong side" classes of bug get named. |
|
||||
|
||||
@@ -174,6 +174,7 @@ procd_add_jail sing-box-c log procfs
|
||||
procd_add_jail_mount var/run/homeproxy-pro/sing-box-c.json
|
||||
procd_add_jail_mount_rw var/run/homeproxy-pro/sing-box-c.log
|
||||
procd_add_jail_mount etc/homeproxy-pro/
|
||||
procd_add_jail_mount_rw etc/homeproxy-pro/cache.db
|
||||
procd_add_jail_mount etc/homeproxy-pro/certs/
|
||||
procd_add_jail_mount /etc/acme/
|
||||
procd_add_jail_mount /etc/ssl/
|
||||
|
||||
@@ -354,6 +354,14 @@ echo "== resource blob digest helper (B2) =="
|
||||
# repository's real resource files against git when the host has it.
|
||||
sh "$ROOT/tests/ucode/test_resource_blob_sha.sh" "$ROOT" "$WORK/resource_blob_sha" || FAILED=1
|
||||
|
||||
echo "== china ip rule-set generator =="
|
||||
# The route side's mainland split is decided from a local rule-set generated out
|
||||
# of china_ip4.txt - the same list the firewall renders its nft set from. One
|
||||
# malformed entry makes sing-box reject the whole rule-set and the client
|
||||
# refuses to start, and the list is replaced unattended, so the generator's
|
||||
# validation and its atomic write are both load-bearing.
|
||||
sh "$ROOT/tests/ucode/test_china_ip_ruleset.sh" "$ROOT" "$WORK/china_ip_ruleset" || FAILED=1
|
||||
|
||||
echo "== test doubles still match production =="
|
||||
# The mocks copy isEmpty/decodeBase64Str/parseURL/redactUrl from production and
|
||||
# their headers say to keep them in sync; nothing enforced it, and the fetcher's
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# runtime/china_ip_ruleset.uc: the text -> sing-box local rule-set generator
|
||||
# that keeps the route side and the firewall deciding mainland China from one
|
||||
# list.
|
||||
#
|
||||
# Why this has its own test: sing-box parses the generated file with
|
||||
# netip.ParsePrefix(), which is strict, and one bad entry makes it reject the
|
||||
# *whole* rule-set - taking the client's start with it ("initialize router:
|
||||
# parse rule-set[0]: ... ParseAddr: IPv4 field has value >255", reproduced on
|
||||
# the target). The source list is downloaded and replaced unattended, so the
|
||||
# failure would arrive on a router nobody is watching. The cases below are the
|
||||
# ones that decide whether that can happen: a malformed line must be skipped,
|
||||
# the file must be replaced atomically (the running instance watches it), and
|
||||
# an empty or unreadable source must fail rather than install an empty rule-set
|
||||
# that silently matches nothing.
|
||||
#
|
||||
# Usage: sh tests/ucode/test_china_ip_ruleset.sh <repo-root> [workdir]
|
||||
|
||||
set -u
|
||||
|
||||
ROOT="$(cd "${1:-$(dirname "$0")/../..}" && pwd)"
|
||||
WORK="${2:-/tmp/hp-china-ip-test}"
|
||||
|
||||
if ! command -v ucode > "/dev/null" 2>&1; then
|
||||
echo "NOT RUN: ucode is not on PATH."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
HELPER="$ROOT/root/etc/homeproxy-pro/scripts/runtime/china_ip_ruleset.uc"
|
||||
rm -rf "$WORK"
|
||||
mkdir -p "$WORK"
|
||||
|
||||
FAILED=0
|
||||
CHECKS=0
|
||||
FAILURES=0
|
||||
|
||||
expect() {
|
||||
# expect <name> <actual> <expected>
|
||||
CHECKS=$((CHECKS + 1))
|
||||
if [ "$2" = "$3" ]; then
|
||||
echo "PASS: $1"
|
||||
else
|
||||
echo "FAIL: $1 (expected '$3', got '$2')"
|
||||
FAILED=1
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
generate() {
|
||||
# generate <source> <destination>; stdout is "N prefixes", status 0/1
|
||||
ucode -S "$HELPER" "$1" "$2" 2>"/dev/null"
|
||||
}
|
||||
|
||||
# --- fixtures ---------------------------------------------------------------
|
||||
|
||||
# A list shaped like the real one: comments, blank lines, valid IPv4 and IPv6,
|
||||
# and five ways a line can be malformed.
|
||||
cat > "$WORK/mixed.txt" <<'EOF'
|
||||
# comment
|
||||
; also a comment
|
||||
|
||||
1.1.8.0/24
|
||||
2.2.2.2/32
|
||||
10.0.0.0/8
|
||||
2001:db8::/32
|
||||
fe80::/10
|
||||
999.1.1.1/24
|
||||
01.2.3.4/24
|
||||
1.1.8.0/33
|
||||
fe80::/129
|
||||
garbage
|
||||
1.2.3.4
|
||||
EOF
|
||||
|
||||
# Five survive: 1.1.8.0/24, 2.2.2.2/32, 10.0.0.0/8, 2001:db8::/32, fe80::/10.
|
||||
expect "mixed list: only the valid entries survive" \
|
||||
"$(generate "$WORK/mixed.txt" "$WORK/mixed.json")" "5 prefixes"
|
||||
expect "mixed list: the out-of-range octet is gone" \
|
||||
"$(grep -c '999\.1\.1\.1' "$WORK/mixed.json")" "0"
|
||||
expect "mixed list: the leading-zero address is gone" \
|
||||
"$(grep -c '01\.2\.3\.4' "$WORK/mixed.json")" "0"
|
||||
expect "mixed list: the /33 prefix is gone" \
|
||||
"$(grep -c '1\.1\.8\.0/33' "$WORK/mixed.json")" "0"
|
||||
expect "mixed list: the /129 prefix is gone" \
|
||||
"$(grep -c 'fe80::/129' "$WORK/mixed.json")" "0"
|
||||
expect "mixed list: a bare address is gone" \
|
||||
"$(grep -c '"1\.2\.3\.4"' "$WORK/mixed.json")" "0"
|
||||
expect "mixed list: the valid IPv4 entries stay" \
|
||||
"$(grep -c '1\.1\.8\.0/24\|2\.2\.2\.2/32\|10\.0\.0\.0/8' "$WORK/mixed.json")" "3"
|
||||
expect "mixed list: the valid IPv6 entry stays" \
|
||||
"$(grep -c '2001:db8::/32' "$WORK/mixed.json")" "1"
|
||||
|
||||
# The rule-set has to be one `ip_cidr` array: one netipx.IPSet on the sing-box
|
||||
# side, not N rules to walk per packet.
|
||||
expect "output is a single ip_cidr rule" \
|
||||
"$(grep -c '"ip_cidr"' "$WORK/mixed.json")" "1"
|
||||
expect "output carries a version" \
|
||||
"$(grep -c '"version": 3' "$WORK/mixed.json")" "1"
|
||||
|
||||
# --- the real bundled list --------------------------------------------------
|
||||
|
||||
# The list this ships with is what the firewall renders its nft set from; a
|
||||
# generator that mangles it would break both sides at once.
|
||||
expect "the bundled china_ip4.txt generates" \
|
||||
"$(generate "$ROOT/root/etc/homeproxy-pro/resources/china_ip4.txt" "$WORK/real.json")" "4068 prefixes"
|
||||
# 8.152.0.0/13 is the range geoip-cn.srs does *not* carry, and the one that
|
||||
# made segmentfault.com and ctrip.com resolve into the proxy path.
|
||||
expect "the bundled list carries 8.152.0.0/13" \
|
||||
"$(grep -c '8\.152\.0\.0/13' "$WORK/real.json")" "1"
|
||||
|
||||
# --- failure paths ----------------------------------------------------------
|
||||
|
||||
# An empty source must not install an empty rule-set: the caller would then be
|
||||
# running a split that matches nothing while looking like it worked.
|
||||
: > "$WORK/empty.txt"
|
||||
expect "empty source produces no file" "$(generate "$WORK/empty.txt" "$WORK/empty.json")" ""
|
||||
ucode -S "$HELPER" "$WORK/empty.txt" "$WORK/empty.json" >/dev/null 2>&1
|
||||
expect "empty source exits non-zero" "$?" "1"
|
||||
expect "empty source wrote nothing" "$(test -e "$WORK/empty.json" && echo yes || echo no)" "no"
|
||||
|
||||
ucode -S "$HELPER" "$WORK/does-not-exist.txt" "$WORK/missing.json" >/dev/null 2>&1
|
||||
expect "unreadable source exits non-zero" "$?" "1"
|
||||
expect "unreadable source wrote nothing" "$(test -e "$WORK/missing.json" && echo yes || echo no)" "no"
|
||||
|
||||
ucode -S "$HELPER" >/dev/null 2>&1
|
||||
expect "no arguments exits non-zero" "$?" "1"
|
||||
|
||||
# --- atomicity --------------------------------------------------------------
|
||||
|
||||
# The running instance watches this path with fswatch, so a partial write would
|
||||
# be read as a broken rule-set. The generator writes a sibling temp file and
|
||||
# renames it, which is why no `*.tmp` may survive a successful run.
|
||||
rm -f "$WORK/real.json" "$WORK/real.json.tmp"
|
||||
generate "$ROOT/root/etc/homeproxy-pro/resources/china_ip4.txt" "$WORK/real.json" >"/dev/null"
|
||||
expect "a successful run leaves no temp file" \
|
||||
"$(ls "$WORK"/real.json.tmp 2>/dev/null | wc -l)" "0"
|
||||
expect "a successful run leaves the rule-set" \
|
||||
"$(test -s "$WORK/real.json" && echo yes || echo no)" "yes"
|
||||
|
||||
# A failed run must not damage an existing rule-set either: the destination is
|
||||
# only touched by the rename.
|
||||
cp "$WORK/real.json" "$WORK/keep.json"
|
||||
ucode -S "$HELPER" "$WORK/empty.txt" "$WORK/keep.json" >/dev/null 2>&1
|
||||
expect "a failed run leaves the previous rule-set in place" \
|
||||
"$(test -s "$WORK/keep.json" && echo yes || echo no)" "yes"
|
||||
expect "a failed run leaves no temp file behind" \
|
||||
"$(ls "$WORK"/keep.json.tmp 2>/dev/null | wc -l)" "0"
|
||||
|
||||
echo
|
||||
echo "$CHECKS checks, $FAILURES failures"
|
||||
[ "$FAILED" -eq 0 ] && echo "CHINA IP RULESET TESTS PASSED"
|
||||
exit "$FAILED"
|
||||
@@ -68,6 +68,15 @@ stage_case() {
|
||||
printf '%s\n' "$DIRECT_DOMAIN" > "$dir/resources/direct_list.txt"
|
||||
printf '%s\n' "$PROXY_DOMAIN" > "$dir/resources/proxy_list.txt"
|
||||
|
||||
# The client config carries a `type: local` rule-set generated from
|
||||
# china_ip4.txt; sing-box opens that path during `check`, and on a router
|
||||
# hp_prepare_runtime_files generates it before the config is used.
|
||||
if ! ucode -S "$ROOT/root/etc/homeproxy-pro/scripts/runtime/china_ip_ruleset.uc" \
|
||||
"$ROOT/root/etc/homeproxy-pro/resources/china_ip4.txt" "$dir/resources/china_ip4.json" \
|
||||
>>"$dir/resources/china_ip4.log" 2>&1; then
|
||||
printf '{"version":3,"rules":[{"ip_cidr":["192.0.2.0/24"]}]}\n' > "$dir/resources/china_ip4.json"
|
||||
fi
|
||||
|
||||
VALIDATE_DATA="${HP_VALIDATE_DATA:-/sbin/validate_data}"
|
||||
sed -e "s#^export const HP_DIR = '/etc/homeproxy-pro';#export const HP_DIR = '$dir';#" \
|
||||
-e "s#^export const RUN_DIR = '/var/run/homeproxy-pro';#export const RUN_DIR = '$dir/run';#" \
|
||||
@@ -113,6 +122,68 @@ stage_case() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# The mode's default policy, asserted as a *pair*. The DNS chain and the
|
||||
# route chain each end in a fallback (`dns.final`, `route.final`), and they
|
||||
# have to name the same side: a domain that ends up proxied must be resolved
|
||||
# by the proxy-path resolver, and one that ends up direct must not be
|
||||
# resolved through the proxy. The two were literals in different modules with
|
||||
# nothing tying them together, and the route side applied "unknown goes to the
|
||||
# proxy" to all four modes - so "Only proxy mainland China" proxied everything
|
||||
# it was not told to proxy.
|
||||
check_fallback_pair() {
|
||||
label="$1"
|
||||
json="$2"
|
||||
|
||||
case "$label" in
|
||||
proxy_mainland_china) want_route="direct-out"; want_dns="default-dns" ;;
|
||||
*) want_route="main-out"; want_dns="main-dns" ;;
|
||||
esac
|
||||
|
||||
# The two finals sit in different blocks and their relative order is not
|
||||
# fixed (route.final is emitted before dns.final in the modes whose dns
|
||||
# block grows, after it in the others), so each is taken from its own
|
||||
# block: dns.final is the only '"final"' indented by two tabs inside the
|
||||
# top-level dns object, route.final the only one inside route.
|
||||
have_dns="$(awk '/^\t"dns": \{/{d=1} d && /^\t\t"final":/{gsub(/.*: "|".*/,""); print; exit}' "$json")"
|
||||
have_route="$(awk '/^\t"route": \{/{r=1} r && /^\t\t"final":/{gsub(/.*: "|".*/,""); print; exit}' "$json")"
|
||||
|
||||
if [ "$have_route" != "$want_route" ]; then
|
||||
echo "FAIL: $label: route.final is '$have_route', the mode's policy is '$want_route'"
|
||||
return 1
|
||||
fi
|
||||
if [ "$have_dns" != "$want_dns" ]; then
|
||||
echo "FAIL: $label: dns.final is '$have_dns', the mode's policy is '$want_dns'"
|
||||
return 1
|
||||
fi
|
||||
# Stated as its own property, because the failure it catches is silent:
|
||||
# if one of the two ever gains a new value, the pair is what breaks.
|
||||
proxy_route=no; proxy_dns=no
|
||||
[ "$have_route" = "main-out" ] && proxy_route=yes
|
||||
[ "$have_dns" = "main-dns" ] && proxy_dns=yes
|
||||
if [ "$proxy_route" != "$proxy_dns" ]; then
|
||||
echo "FAIL: $label: route.final ($have_route) and dns.final ($have_dns) disagree on the mode's default"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "PASS: $label: route.final=$have_route and dns.final=$have_dns agree"
|
||||
|
||||
# Both mainland modes match destinations against geoip-cn, so both have to
|
||||
# declare it. proxy_mainland_china reached this point without the
|
||||
# declaration once and sing-box refused the whole config with
|
||||
# "initialize rule[3]: rule-set not found: geoip-cn"; the health gate then
|
||||
# rolled the intercept layer back and the network went unproxied.
|
||||
case "$label" in
|
||||
bypass_mainland_china|proxy_mainland_china)
|
||||
if ! grep -qF '"rule_set": "geoip-cn"' "$json" || ! grep -qF '"tag": "geoip-cn"' "$json"; then
|
||||
echo "FAIL: $label: the route rule matches geoip-cn but the rule-set is not declared"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# The rule_set assertions, shared by every case: the list reaches the config,
|
||||
# the DNS half resolves through main-dns ahead of the SVCB/HTTPS reject, and
|
||||
# direct-domain keeps its precedence.
|
||||
@@ -232,6 +303,7 @@ for mode in bypass_mainland_china global gfwlist proxy_mainland_china; do
|
||||
else
|
||||
FAILED=1
|
||||
fi
|
||||
check_fallback_pair "$mode" "$dir/run/sing-box-c.json"
|
||||
else
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
@@ -49,6 +49,19 @@ run_case() {
|
||||
: > "$dir/resources/direct_list.txt"
|
||||
: > "$dir/resources/proxy_list.txt"
|
||||
|
||||
# The client config now carries a `type: local` rule-set generated from
|
||||
# china_ip4.txt (the same list the firewall renders its nft set from, so
|
||||
# both sides decide mainland-China from one source). sing-box opens that
|
||||
# path during `check`, so the staged tree has to have it; on a router
|
||||
# hp_prepare_runtime_files generates it before the config is used.
|
||||
if ! ucode -S "$ROOT/root/etc/homeproxy-pro/scripts/runtime/china_ip_ruleset.uc" \
|
||||
"$ROOT/root/etc/homeproxy-pro/resources/china_ip4.txt" "$dir/resources/china_ip4.json" \
|
||||
>>"$dir/resources/china_ip4.log" 2>&1; then
|
||||
# Most fixtures do not exercise the route side; an empty rule-set
|
||||
# keeps `sing-box check` happy without inventing data.
|
||||
printf '{"version":3,"rules":[{"ip_cidr":["192.0.2.0/24"]}]}\n' > "$dir/resources/china_ip4.json"
|
||||
fi
|
||||
|
||||
if grep -q "__RULESET_DIR__" "$fixture"; then
|
||||
# The fixture needs a real local rule-set on disk. The path must
|
||||
# live under /tmp/homeproxy_* (validateHomeProxyPath() in
|
||||
@@ -277,6 +290,34 @@ if ! grep -q '"detour": "main-out"' "$WORK/client/run/sing-box-c.json"; then
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
# The mainland split must be decided from the same list the firewall renders
|
||||
# its nft set from. geoip-cn.srs (upstream) and china_ip4.txt (ours) disagree
|
||||
# on real ranges - the bundled list carries 8.152.0.0/13, geoip-cn.srs does
|
||||
# not - so a destination in that range passes the firewall as "not mainland"
|
||||
# and must still be sent direct by the route rule. The local rule-set is
|
||||
# generated from china_ip4.txt by runtime/china_ip_ruleset.uc and watched with
|
||||
# fswatch, so a resource update needs no restart.
|
||||
if ! grep -q '"path": "'"$WORK"'/client/resources/china_ip4.json"' "$WORK/client/run/sing-box-c.json"; then
|
||||
echo "FAIL: client: the mainland route rule does not read the generated china_ip4 rule-set"
|
||||
FAILED=1
|
||||
fi
|
||||
if ! grep -q '"tag": "china-ip"' "$WORK/client/run/sing-box-c.json"; then
|
||||
echo "FAIL: client: the local china-ip rule-set is missing from the generated config"
|
||||
FAILED=1
|
||||
fi
|
||||
# ... and it has to be a rule the route layer actually consults, not a
|
||||
# declaration sing-box loads and never matches.
|
||||
if ! grep -A2 '"rule_set": "china-ip"' "$WORK/client/run/sing-box-c.json" | grep -q '"outbound": "direct-out"'; then
|
||||
echo "FAIL: client: no route rule sends the china-ip rule-set to direct-out"
|
||||
FAILED=1
|
||||
fi
|
||||
# The generated file itself must hold the list, or the node would run with a
|
||||
# split that matches nothing.
|
||||
if ! grep -q '8\.152\.0\.0/13' "$WORK/client/resources/china_ip4.json"; then
|
||||
echo "FAIL: client: the generated china_ip4 rule-set does not carry the bundled list"
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
# --- the product default proxy mode --------------------------------------
|
||||
#
|
||||
# client.uci runs `proxy_mode 'tun'`, where self_mark is empty and no outbound
|
||||
|
||||
Reference in new issue
Block a user