mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 16:44:23 +08:00
334 lines
14 KiB
Bash
334 lines
14 KiB
Bash
#!/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, which makes the ISP the
|
|
# answerer of the one query that decides where every proxied name goes.
|
|
# The resolver is derived from the China DNS server now, so both halves
|
|
# of that rule are asserted: a bare IP there yields a bootstrap-dns that
|
|
# main-dns points at and which carries no domain_resolver of its own,
|
|
# and a hostname there yields no bootstrap server at all (a hostname
|
|
# cannot resolve the hostname it would be needed to resolve).
|
|
#
|
|
# 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"
|
|
CHINA_DNS_IP="223.5.5.5"
|
|
CHINA_DNS_DOH="https://dns.alidns.com/dns-query"
|
|
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 fixture carries no china_dns_server (the generator then falls
|
|
# back to the Aliyun public resolver), so the line is inserted into the
|
|
# `config` section here - the section the Loader reads it from. It has to be
|
|
# inserted rather than substituted for that reason.
|
|
stage_case() {
|
|
dir="$1"
|
|
label="$2"
|
|
china_dns="$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 "$china_dns" ]; then
|
|
sed -i "/^\toption routing_mode /i\\
|
|
\toption china_dns_server '$china_dns'
|
|
" "$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"
|
|
|
|
# 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
|
|
|
|
# The DNS half of the China split. generate_client.uc reports it as
|
|
# ctx.china_domain_ready, an lstat on this file; absent, the domestic
|
|
# lookup rule is not emitted and the ordering this file asserts cannot be
|
|
# observed at all. On a router hp_prepare_runtime_files generates it from
|
|
# china_list.txt by the same route as china_ip4.json above.
|
|
if ! ucode -S "$ROOT/root/etc/homeproxy-pro/scripts/runtime/domain_ruleset.uc" \
|
|
"$ROOT/root/etc/homeproxy-pro/resources/china_list.txt" "$dir/resources/china-domain.json" \
|
|
>>"$dir/resources/china-domain.log" 2>&1; then
|
|
printf '{"version":3,"rules":[{"domain_suffix":["example.invalid"]}]}\n' > "$dir/resources/china-domain.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';#" \
|
|
-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 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 the local china-ip
|
|
# rule-set, 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"; 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": "china-ip"' "$json" || ! grep -qF '"tag": "china-ip"' "$json"; then
|
|
echo "FAIL: $label: the route rule matches china-ip 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.
|
|
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*)
|
|
china_domain_rule="$(grep -nF '"rule_set": "china-domain"' "$json" | head -1 | cut -d: -f1)"
|
|
if [ -z "$china_domain_rule" ] || [ "$china_domain_rule" -lt "$dns_rule" ]; then
|
|
echo "FAIL: $label: china-domain must come after proxy-domain (proxy=$dns_rule china-domain=${china_domain_rule:-none})"
|
|
return 1
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
return 0
|
|
}
|
|
|
|
# --- 1. bootstrap resolver, derived from a bare-IP China DNS server ---------
|
|
|
|
dir="$WORK/bootstrap"
|
|
if stage_case "$dir" "bootstrap" "$CHINA_DNS_IP" "$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: a bare-IP China DNS server produced no bootstrap-dns server"
|
|
FAILED=1
|
|
elif ! sed -n "$bootstrap_line,$((bootstrap_line + 4))p" "$json" | grep -qF "\"$CHINA_DNS_IP\""; then
|
|
echo "FAIL: bootstrap: bootstrap-dns does not carry the China DNS server ($CHINA_DNS_IP)"
|
|
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 ($CHINA_DNS_IP)"
|
|
fi
|
|
fi
|
|
else
|
|
FAILED=1
|
|
fi
|
|
|
|
# --- 2. a hostname China DNS server is not a usable bootstrap --------------
|
|
# The option also accepts a DoH/DoT URL, and a URL is a hostname: using one to
|
|
# resolve the hostname it would be needed to resolve is the cycle this whole
|
|
# mechanism exists to avoid. It must fall back to the WAN resolver, loudly
|
|
# enough to be visible here, rather than to a broken bootstrap.
|
|
|
|
dir="$WORK/hostname"
|
|
if stage_case "$dir" "hostname" "$CHINA_DNS_DOH" "$FIXTURE"; then
|
|
json="$dir/run/sing-box-c.json"
|
|
if grep -qF '"tag": "bootstrap-dns"' "$json"; then
|
|
echo "FAIL: hostname: a DoH China DNS server was used as the bootstrap resolver"
|
|
FAILED=1
|
|
elif ! grep -qF '"server": "default-dns"' "$json"; then
|
|
echo "FAIL: hostname: no bootstrap server and main-dns does not use default-dns either"
|
|
FAILED=1
|
|
else
|
|
echo "PASS: hostname: a DoH China DNS server yields no bootstrap server, main-dns uses 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
|
|
check_fallback_pair "$mode" "$dir/run/sing-box-c.json"
|
|
else
|
|
FAILED=1
|
|
fi
|
|
done
|
|
|
|
exit $FAILED
|