mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-05 18:44:03 +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
@@ -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