Files
op-packages/luci-app-homeproxy-pro/tests/ucode/test_domain_ruleset.sh
T

185 lines
7.2 KiB
Bash
Executable File

#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
#
# runtime/domain_ruleset.uc: the text -> sing-box local rule-set generator for
# the DNS half of the China split.
#
# Why this has its own test: it is the only thing standing between a downloaded
# host-name list and a DNS split. sing-box rejects a rule-set containing an
# entry it cannot parse, and it rejects the WHOLE file when it does - so one bad
# line in a list that is replaced unattended, on a router nobody is watching,
# takes the resolver's half of the split down with it. The cases below are the
# ones that decide whether that can happen.
#
# Three properties matter beyond "it produces a file":
#
# - the entry has to be a SUFFIX. `domain` would match only the name itself
# and quietly stop matching www.<domain> for the whole list, which is a
# split that looks configured and behaves as if the list were empty.
# - the host-name check has to be in-process. validation() in homeproxy-pro.uc
# shells out to /sbin/validate_data once per call, and this list is 111k
# lines - that is 111k process spawns, which is not a test that passes, it
# is a test that never finishes.
# - an empty or all-malformed source must fail rather than install a
# rule-set that matches nothing.
#
# Usage: sh tests/ucode/test_domain_ruleset.sh <repo-root> [workdir]
set -u
ROOT="$(cd "${1:-$(dirname "$0")/../..}" && pwd)"
WORK="${2:-/tmp/hp-domain-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/domain_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 domain suffixes"
ucode -S "$HELPER" "$1" "$2" 2>"/dev/null"
}
# --- fixtures ---------------------------------------------------------------
# A list shaped like the real one: comments, blank lines, plain two-label
# names, the single-label and deep forms that appear in a real suffix list, and
# the ways a line can be malformed.
#
# Every malformed entry below is named so that its own assertion's grep pattern
# MATCHES it. That is the point: an assertion whose pattern cannot match the
# entry it is about stays green whether or not the entry is rejected, which is
# a guard pointing at the wrong side. The first draft of this fixture had
# "trailing-hyphen.com" and "trailing.dot.com" - both perfectly valid names -
# while the assertions claimed a trailing hyphen and a trailing dot, and both
# assertions were vacuous.
cat > "$WORK/mixed.txt" <<'EOF'
# comment
; also a comment
taobao.com
163.com
0.zone
a.b.c.example.cn
xn--fiqs8s.com
under_score.com
-leading-hyphen.com
trailinghyphen-.com
double..dot.com
.dotted.com
trailingdot.com.
with space.com
http://scheme.com
192.168.1.1
EOF
# Five survive: taobao.com, 163.com, 0.zone, a.b.c.example.cn,
# xn--fiqs8s.com. Everything else in the fixture is one malformation.
expect "mixed list: only the valid entries survive" \
"$(generate "$WORK/mixed.txt" "$WORK/mixed.json")" "5 domain suffixes"
expect "mixed list: the underscore name is gone" \
"$(grep -c 'under_score' "$WORK/mixed.json")" "0"
expect "mixed list: the leading-hyphen label is gone" \
"$(grep -c 'leading-hyphen' "$WORK/mixed.json")" "0"
expect "mixed list: the trailing-hyphen label is gone" \
"$(grep -c 'trailinghyphen-' "$WORK/mixed.json")" "0"
expect "mixed list: the doubled dot is gone" \
"$(grep -c 'double\.\.dot' "$WORK/mixed.json")" "0"
expect "mixed list: the leading dot is gone" \
"$(grep -c '"\.dotted\.com"' "$WORK/mixed.json")" "0"
expect "mixed list: the trailing dot is gone" \
"$(grep -c '"trailingdot\.com\."' "$WORK/mixed.json")" "0"
expect "mixed list: the name with a space is gone" \
"$(grep -c 'with space' "$WORK/mixed.json")" "0"
expect "mixed list: the URL is gone" \
"$(grep -c 'scheme.com' "$WORK/mixed.json")" "0"
expect "mixed list: a bare address is not treated as a domain" \
"$(grep -c '192\.168\.1\.1' "$WORK/mixed.json")" "0"
expect "mixed list: the plain names stay" \
"$(grep -c 'taobao\.com\|163\.com\|0\.zone\|a\.b\.c\.example\.cn\|xn--fiqs8s\.com' "$WORK/mixed.json")" "5"
# The single most consequential property: suffix, not exact.
expect "output is a single domain_suffix rule" \
"$(grep -c '"domain_suffix"' "$WORK/mixed.json")" "1"
expect "output does not use exact domain matching" \
"$(grep -c '"domain":' "$WORK/mixed.json")" "0"
expect "output carries a version" \
"$(grep -c '"version": 3' "$WORK/mixed.json")" "1"
# --- the real bundled list --------------------------------------------------
# This is the list the DNS split runs on. A generator that mangles it would
# send every mainland lookup to the proxy resolver while looking healthy.
if [ -f "$ROOT/root/etc/homeproxy-pro/resources/china_list.txt" ]; then
expect "the bundled china_list.txt generates" \
"$(generate "$ROOT/root/etc/homeproxy-pro/resources/china_list.txt" "$WORK/real.json")" \
"$(wc -l < "$ROOT/root/etc/homeproxy-pro/resources/china_list.txt" | tr -d ' ') domain suffixes"
expect "the bundled list is emitted as suffixes, not exact names" \
"$(grep -c '"domain_suffix"' "$WORK/real.json")" "1"
expect "the bundled list emits no exact-domain rules" \
"$(grep -c '"domain":' "$WORK/real.json")" "0"
else
echo "SKIP: the bundled china_list.txt is not in the tree."
fi
# --- failure paths ----------------------------------------------------------
# An empty source must not install an empty rule-set: the DNS split would then
# match nothing and every mainland lookup would go to the proxy resolver, with
# nothing in the log saying so.
: > "$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"
# A source that is entirely malformed is the same case reached differently.
printf 'http://a\nhttp://b\nunder_score\n' > "$WORK/junk.txt"
expect "all-malformed source produces no file" "$(generate "$WORK/junk.txt" "$WORK/junk.json")" ""
ucode -S "$HELPER" "$WORK/junk.txt" "$WORK/junk.json" >/dev/null 2>&1
expect "all-malformed source exits non-zero" "$?" "1"
# A missing source is a caller error, not an empty list.
expect "missing source exits non-zero" \
"$(ucode -S "$HELPER" "$WORK/no-such-file.txt" "$WORK/none.json" >/dev/null 2>&1; echo $?)" "1"
# --- atomicity --------------------------------------------------------------
# sing-box watches this file and reloads it in place, so a partially written
# one is read by a running instance. The destination has to appear whole or
# not at all.
cat > "$WORK/atomic.txt" <<'EOF'
one.example
two.example
three.example
EOF
generate "$WORK/atomic.txt" "$WORK/atomic.json" >/dev/null
expect "the destination is complete after a run" \
"$(grep -c 'one\.example\|two\.example\|three\.example' "$WORK/atomic.json")" "3"
expect "no temporary file is left behind" \
"$(ls "$WORK" | grep -c 'atomic\.json\.tmp')" "0"
# --- summary ----------------------------------------------------------------
echo " ($CHECKS checks, $FAILURES failures)"
exit "$FAILED"