💐 Sync 2026-09-30 01:19:34

This commit is contained in:
github-actions[bot] committed 2026-09-30 01:19:34 +08:00
1 parent 67427d9030
commit a64d55f7c5
50 files changed
+2837 -589

No files matched your search

+1
View File
@@ -281,6 +281,7 @@ Two further host differences are bridged so the remaining checks still run:
| `tests/ucode/test_parser_flatten.uc` | Round-trip invariant for the canonical-Node pipeline: `flatten(normalize(parse_uri(uri)))` must equal the parser's flat UCI dict field-for-field for every supported scheme. This is what stops the parser, the Loader's `PROTOCOL_OPTIONS` (now derived from `parser/mapping.uc`) and the Repository's flatten from drifting apart. |
| `tests/ucode/test_subscription_repository.uc` | Integration test for `subscription/repository.uc`: stages a sandboxed UCI file with a user node, a kept subscription node, and a dropped subscription node, then runs `Repository.apply_nodes` and asserts (a) the user node is untouched, (b) the kept node has the new fields and no stale fields, (c) the dropped node is gone, (d) a brand-new node is added under `md5(group+label)`. PR-03 added the `apply_main_node_refs` paths (urltest prune + missing-target switch + reset-to-nil) and the `scrub_stale_urltest_refs` pass to the same test. |
| `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`. The `redirect` case is the product's default `proxy_mode` and asserts, from the emitted JSON, that every dialling outbound carries the runtime `routing_mark`. |
| `tests/ucode/test_dns_proxy_list.sh` | The `proxy_list.txt` → `dns.rules` plumbing and the bootstrap resolver, neither of which `run_case()` in the generator suite can see (it truncates both resource lists before every case and the fixture has no `bootstrap_dns` option). Patches the client fixture to each of the four proxy modes with both lists populated, and asserts the DNS rule order (`direct-domain` < `proxy-domain` via `main-dns` < the SVCB/HTTPS reject, `geosite-cn` last when the mode emits it) plus the routing half's `"tag": "proxy-domain"` and `route.final: main-out`. Then, separately: with `bootstrap_dns` set, `main-dns` must resolve its own hostname through `bootstrap-dns` and that server must carry no `domain_resolver`; with it unset, no `bootstrap-dns` server may be emitted at all. |
| `tests/toolchain/build-ucode-linux.sh` | Builds the local ucode toolchain described above; not part of `tests/run.sh`. |
| `tests/toolchain/validate-data.sh` | Off-target stand-in for `/sbin/validate_data`, used through `HP_VALIDATE_DATA`; not part of `tests/run.sh`. |
| `tests/ucode/test_ucode_grammar.sh` | Pins the ucode dialect: the toolchain must reject `export function ... }` without `;` and object/array destructuring, and must accept the constructs the package uses (`?.`/`??`, object spread, computed keys, template literals). |
@@ -3282,7 +3282,7 @@
"kind": "Value",
"name": "dns_server",
"title": "DNS server",
"description": "Support UDP, TCP, DoH, DoQ, DoT. TCP protocol will be used if not specified.",
"description": "Resolves every domain that is not on the China DNS path, through the selected proxy node. Pick an overseas service: a domestic resolver answers these queries with polluted or NXDOMAIN results, which is what makes a foreign site fail to resolve while the proxy itself works. Support UDP, TCP, DoH, DoQ, DoT. TCP protocol will be used if not specified.",
"depends": [
{
"routing_mode": "custom",
@@ -3334,7 +3334,7 @@
"kind": "Value",
"name": "china_dns_server",
"title": "China DNS server",
"description": "The dns server for resolving China domains. Support UDP, TCP, DoH, DoQ, DoT.",
"description": "Resolves the domains on the China path, so it has to be a domestic service to get the correct local CDN answers. Keep the default; the upstream may block UDP/53, in which case a DoH address (https://dns.alidns.com/dns-query, https://doh.pub/dns-query) also works. Support UDP, TCP, DoH, DoQ, DoT.",
"depends": [
[
"routing_mode",
@@ -3387,6 +3387,24 @@
"rmempty": false
}
},
{
"kind": "DynamicList",
"name": "bootstrap_dns",
"title": "Bootstrap DNS server",
"description": "Resolver used to look up the DNS server's own hostname - only relevant when that address is a domain, as with a DoH/DoT endpoint. Leave empty to use the WAN resolver. Only the first entry is used.",
"depends": [
{
"routing_mode": "custom",
"!reverse": true
}
],
"values": [],
"subsection": null,
"props": {
"datatype": "ipaddr",
"modalonly": true
}
},
{
"kind": "SectionValue",
"name": "_dns_cache",
@@ -432,6 +432,13 @@ fi
echo "== generator regression tests =="
sh "$ROOT/tests/ucode/test_generators.sh" "$ROOT" "$WORK/generators" || FAILED=1
echo "== proxy-domain DNS routing and DNS bootstrap =="
# The generator suite truncates the resource lists and has no bootstrap option,
# so it can see neither proxy_list.txt -> dns.rules (per proxy mode: the
# routing half was always mode-agnostic, the DNS half was not) nor the
# bootstrap resolver main-dns uses for its own hostname.
sh "$ROOT/tests/ucode/test_dns_proxy_list.sh" "$ROOT" "$WORK/dns-proxy-list" || FAILED=1
echo "== golden protocol snapshot =="
sh "$ROOT/tests/ucode/test_golden_outbounds.sh" "$ROOT" "$WORK/golden" || FAILED=1
@@ -0,0 +1,240 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
#
# Two properties of the proxy-mode DNS block that the generator regression
# suite cannot see, because run_case() truncates the resource lists and uses a
# fixture without the bootstrap option:
#
# 1. The proxy-domain resource list has to reach the DNS block in *every*
# proxy routing mode, not only in bypass_mainland_china.
#
# proxy_list.txt is the user's escape hatch for a domain the preset lists
# mis-classify - the canonical case is Google Play, whose
# connect.googleapis.cn resolves through the domestic resolver and then
# stays direct, so the store cannot update. The routing half
# (route.uc pushes the proxy-domain rule_set to main-out) has always been
# emitted in all the proxy modes, while the DNS half used to be nested
# inside the bypass_mainland_china branch. In global / gfwlist /
# proxy_mainland_china a listed domain therefore still resolved through
# the domestic resolver; only the bypass mode did the right thing.
#
# 2. The bootstrap resolver: the DoH/DoT endpoint (main-dns) is the only
# server whose address is a hostname, and it used to borrow default-dns
# (the WAN/ISP resolver) for that lookup. When the user configures a
# bootstrap resolver, main-dns must point at it instead, and that server
# must stay at the bottom of the chain (no domain_resolver of its own,
# no DNS rule routing to it) or the lookup depends on itself.
#
# Usage: sh tests/ucode/test_dns_proxy_list.sh <repo-root> [work-dir]
ROOT="${1:-.}"
WORK="${2:-/tmp/hp-dns-proxy-list-test}"
ROOT="$(cd "$ROOT" && pwd)"
FIXTURE="$ROOT/tests/fixtures/generators/client.uci"
DIRECT_DOMAIN="direct.example.cn"
PROXY_DOMAIN="play.example.cn"
BOOTSTRAP_DNS="223.5.5.5"
FAILED=0
rm -rf "$WORK"
mkdir -p "$WORK"
# Stage the checkout the way test_generators.sh does: HP_DIR/RUN_DIR/UCICONFIG_DIR
# are rewritten into the work dir and the fixture is copied in as the UCI
# config. The extra bootstrap line is appended to the `config` section here
# (the section's first named section), which is how the Loader reads it.
stage_case() {
dir="$1"
label="$2"
bootstrap="$3"
fixture="$4"
rm -rf "$dir"
mkdir -p "$dir/config" "$dir/run" "$dir/scripts/config" "$dir/scripts/generator" \
"$dir/scripts/parser" "$dir/resources"
cp "$fixture" "$dir/config/homeproxy-pro"
if [ -n "$bootstrap" ]; then
sed -i "/^\toption routing_mode /i\\
\toption bootstrap_dns '$bootstrap'
" "$dir/config/homeproxy-pro"
fi
# Both lists are populated on purpose. direct_list.txt is what makes the
# direct-domain DNS rule appear at all, and the generator documents that
# proxy-domain sits after it; with only one of the two the relative order
# is untestable.
printf '%s\n' "$DIRECT_DOMAIN" > "$dir/resources/direct_list.txt"
printf '%s\n' "$PROXY_DOMAIN" > "$dir/resources/proxy_list.txt"
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';#" \
-e "s#^export const UCICONFIG_DIR = '/etc/config';#export const UCICONFIG_DIR = '$dir/config';#" \
-e "s#/sbin/validate_data#${VALIDATE_DATA}#" \
"$ROOT/root/etc/homeproxy-pro/scripts/homeproxy-pro.uc" > "$dir/scripts/homeproxy-pro.uc"
cp "$ROOT/root/etc/homeproxy-pro/scripts/config/loader.uc" "$dir/scripts/config/"
cp "$ROOT/root/etc/homeproxy-pro/scripts/config/model.uc" "$dir/scripts/config/"
cp "$ROOT/root/etc/homeproxy-pro/scripts/config/adapter.uc" "$dir/scripts/config/"
cp "$ROOT/root/etc/homeproxy-pro/scripts/parser/"*.uc "$dir/scripts/parser/"
cp "$ROOT/root/etc/homeproxy-pro/scripts/generator/"*.uc "$dir/scripts/generator/"
cp "$ROOT/root/etc/homeproxy-pro/scripts/generate_client.uc" "$dir/scripts/generate_client.uc"
# macOS: `sing-box check` rejects the SO_MARK-based routing_mark on a
# direct outbound (Linux-only), so the generator copy emits null there.
# Same patch as test_generators.sh, applied to the same module.
case "$(uname -s)" in
Darwin)
sed -i '' "s#routing_mark: strToInt(.*self_mark)#routing_mark: null#" \
"$dir/scripts/generator/client.uc"
;;
esac
if ! ( cd "$dir/scripts" && ucode -L "$dir/scripts" generate_client.uc > "$dir/generate.out" 2> "$dir/generate.err" ); then
echo "FAIL: $label: generate_client.uc exited non-zero"
head -5 "$dir/generate.err"
return 1
fi
json="$dir/run/sing-box-c.json"
if [ ! -s "$json" ]; then
echo "FAIL: $label: no client configuration was generated"
head -5 "$dir/generate.err"
return 1
fi
if ! sing-box check --config "$json"; then
echo "FAIL: $label: sing-box rejected the generated configuration"
return 1
fi
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.
check_proxy_list() {
label="$1"
json="$2"
if ! grep -qF "$PROXY_DOMAIN" "$json"; then
echo "FAIL: $label: proxy_list.txt never reached the generated config"
return 1
fi
dns_rule="$(grep -nF '"rule_set": "proxy-domain"' "$json" | head -1 | cut -d: -f1)"
if [ -z "$dns_rule" ]; then
echo "FAIL: $label: no proxy-domain rule_set in the generated config"
return 1
fi
if ! sed -n "$((dns_rule + 1)),$((dns_rule + 4))p" "$json" | grep -qF '"main-dns"'; then
echo "FAIL: $label: the proxy-domain DNS rule does not resolve through main-dns:"
sed -n "$((dns_rule - 3)),$((dns_rule + 4))p" "$json"
return 1
fi
reject_line="$(grep -nF '"query_type": [' "$json" | head -1 | cut -d: -f1)"
if [ -n "$reject_line" ] && [ "$dns_rule" -gt "$reject_line" ]; then
echo "FAIL: $label: the proxy-domain DNS rule is emitted after the SVCB/HTTPS reject"
return 1
fi
# A domain present in both lists keeps direct-domain first: the
# relocation into the shared prefix must not have reversed the
# precedence the bypass block used to have.
direct_rule="$(grep -nF '"rule_set": "direct-domain"' "$json" | head -1 | cut -d: -f1)"
if [ -z "$direct_rule" ] || [ "$direct_rule" -gt "$dns_rule" ]; then
echo "FAIL: $label: direct-domain must be the first DNS rule (direct=${direct_rule:-none} proxy=$dns_rule)"
return 1
fi
# In the one mode that has both, the domestic lookup still comes last.
case "$label" in
bypass_mainland_china*)
geosite_rule="$(grep -nF '"rule_set": "geosite-cn"' "$json" | head -1 | cut -d: -f1)"
if [ -z "$geosite_rule" ] || [ "$geosite_rule" -lt "$dns_rule" ]; then
echo "FAIL: $label: geosite-cn must come after proxy-domain (proxy=$dns_rule geosite=${geosite_rule:-none})"
return 1
fi
;;
esac
return 0
}
# --- 1. bootstrap resolver -------------------------------------------------
dir="$WORK/bootstrap"
if stage_case "$dir" "bootstrap" "$BOOTSTRAP_DNS" "$FIXTURE"; then
json="$dir/run/sing-box-c.json"
bootstrap_line="$(grep -nF '"tag": "bootstrap-dns"' "$json" | head -1 | cut -d: -f1)"
if [ -z "$bootstrap_line" ]; then
echo "FAIL: bootstrap: no bootstrap-dns server was emitted"
FAILED=1
elif ! sed -n "$bootstrap_line,$((bootstrap_line + 4))p" "$json" | grep -qF "\"$BOOTSTRAP_DNS\""; then
echo "FAIL: bootstrap: bootstrap-dns does not carry $BOOTSTRAP_DNS"
FAILED=1
elif sed -n "$bootstrap_line,$((bootstrap_line + 4))p" "$json" | grep -qF '"domain_resolver"'; then
echo "FAIL: bootstrap: the bootstrap resolver has a domain_resolver of its own"
FAILED=1
else
# main-dns is the only hostname-addressed server, so it is the only
# one allowed to point at the bootstrap resolver.
main_line="$(grep -nF '"tag": "main-dns"' "$json" | head -1 | cut -d: -f1)"
if [ -z "$main_line" ]; then
echo "FAIL: bootstrap: no main-dns server was emitted"
FAILED=1
elif ! sed -n "$main_line,$((main_line + 6))p" "$json" | grep -qF '"server": "bootstrap-dns"'; then
echo "FAIL: bootstrap: main-dns still resolves through the WAN resolver"
FAILED=1
else
echo "PASS: bootstrap: main-dns resolves its own hostname through bootstrap-dns ($BOOTSTRAP_DNS)"
fi
fi
else
FAILED=1
fi
# --- 2. the default (no bootstrap configured) ------------------------------
dir="$WORK/default"
if stage_case "$dir" "default" "" "$FIXTURE"; then
json="$dir/run/sing-box-c.json"
if grep -qF '"tag": "bootstrap-dns"' "$json"; then
echo "FAIL: default: an unconfigured bootstrap resolver was emitted"
FAILED=1
elif ! grep -qF '"server": "default-dns"' "$json"; then
echo "FAIL: default: main-dns no longer resolves through the WAN resolver"
FAILED=1
else
echo "PASS: default: no bootstrap server, main-dns still resolves through default-dns"
fi
else
FAILED=1
fi
# --- 3. proxy-domain in every proxy routing mode ---------------------------
# One case per proxy routing mode. Custom mode is deliberately absent: it
# ignores both resource lists (context.uc gates their inputs on
# `routing_mode !== 'custom'`) and has its own user-defined dns_rule sections.
for mode in bypass_mainland_china global gfwlist proxy_mainland_china; do
dir="$WORK/$mode"
sed "s#^\(\s*option routing_mode \).*#\1'$mode'#" "$FIXTURE" > "$WORK/fixture-$mode.uci"
if stage_case "$dir" "$mode" "" "$WORK/fixture-$mode.uci"; then
if check_proxy_list "$mode" "$dir/run/sing-box-c.json"; then
echo "PASS: $mode: direct-domain < proxy-domain via main-dns < SVCB/HTTPS reject"
else
FAILED=1
fi
else
FAILED=1
fi
done
exit $FAILED