🎈 Sync 2026-10-04 14:38:18

This commit is contained in:
github-actions[bot] committed 2026-10-04 14:38:18 +08:00
1 parent 0e6611f767
commit aba12667c5
88 files changed
+4600 -1458

No files matched your search

+1 -1
View File
@@ -282,7 +282,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 `china_dns_server` 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, the bootstrap resolver: it is derived from `china_dns_server`, so a bare IP there must make `main-dns` resolve its own hostname through `bootstrap-dns` with no `domain_resolver` of its own, and a DoH URL there (a hostname, which cannot resolve a hostname) must emit no `bootstrap-dns` server at all and leave `main-dns` on `default-dns`. |
| `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 `china_dns_server` 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, `china-domain` last when the mode emits it) plus the routing half's `"tag": "proxy-domain"` and `route.final: main-out`. Then, separately, the bootstrap resolver: it is derived from `china_dns_server`, so a bare IP there must make `main-dns` resolve its own hostname through `bootstrap-dns` with no `domain_resolver` of its own, and a DoH URL there (a hostname, which cannot resolve a hostname) must emit no `bootstrap-dns` server at all and leave `main-dns` on `default-dns`. |
| `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). |
+209 -91
View File
@@ -2223,9 +2223,9 @@ fi
# proxy resolver even though the route side would now send it direct.
cn_fb_block="$(sed -n '/cn_fallback/,/^ }/p' "$SCRIPTS/generator/dns.uc" || true)"
if printf '%s' "$cn_fb_block" | grep -q "china-ip6"; then
pass "the cn-fallback response match covers china-ip6 as well as geoip-cn"
pass "the cn-fallback response match covers china-ip6 as well as china-ip"
else
fail "cn-fallback still matches geoip-cn only; the DNS half of the IPv6 split"
fail "cn-fallback still matches china-ip only; the DNS half of the IPv6 split"
fail "is not in agreement with the route half"
fi
@@ -2626,118 +2626,115 @@ else
fi
echo
echo "== guard 52: the startup fallback stays opt-in, and the two sides stay in step =="
echo "== guard 52: the China split is local, and both halves read one list =="
# ruleset_safe_start points every remote rule-set that has no initial file of
# its own at an EMPTY rule-set, so sing-box starts immediately instead of
# fetching during initialization - before the inbounds bind. On a cold cache
# that is the difference between a router that comes up and one that waits on
# raw.githubusercontent.com, through the node, before it will listen at all.
# The split that keeps mainland traffic direct used to be fed by two remote
# rule-sets downloaded from SagerNet's repositories: geoip-cn on the route
# side, geosite-cn on the DNS side. Three things were wrong with that and all
# three are silent when they come back:
#
# It is opt-in because the fallback is empty on purpose: a rule-set with no
# rules matches nothing, so everything it would have split falls through to
# `final`. On bypass_mainland_china that means mainland destinations can be
# proxied until the download succeeds. That is a routing change, so the
# default must stay off, exactly as sniffer_advanced_mode's does.
# - a cold start had to fetch them before the inbounds bind, through the
# node, so the proxy's own start depended on the CDN being reachable
# through the proxy;
# - the kernel's half (homeproxy_mainland_addr_v4, from china_ip4.txt) and
# the resolver's half came from different files, and route.uc once carried
# a corrective rule plus a long comment because they disagreed;
# - every day of that, the resource lists behind the kernel's half were only
# refreshed if the user happened to have subscription auto-update on.
#
# The rest of the guard is about the mechanism being one mechanism: the CLI
# writes the file and the generator points at it, and a disagreement between
# them is silent and fatal (a wrong-format or missing initial file is ignored
# by sing-box, which then blocks startup exactly as if none existed).
SAFE_START="$(python3 - "$ROOT" "$SCRIPTS" <<'PY'
# They are local files now, each generated from a list the resource updater
# maintains, and the firewall renders its nft set from those same lists. This
# guard pins the arrangement rather than the feature it replaced: the tags the
# two halves name have to be the same tags route.uc declares, they have to be
# declared local, and nothing may reintroduce a remote download for them.
CN_LOCAL="$(python3 - "$ROOT" "$SCRIPTS" <<'PY'
import pathlib, re, sys
root, scripts = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2])
config_uc = (root / 'root/etc/config/homeproxy-pro').read_text(encoding='utf-8')
context_uc = (scripts / 'generator/context.uc').read_text(encoding='utf-8')
route_uc = (scripts / 'generator/route.uc').read_text(encoding='utf-8')
ruleset_uc = (scripts / 'generator/ruleset.uc').read_text(encoding='utf-8')
dns_uc = (scripts / 'generator/dns.uc').read_text(encoding='utf-8')
common_uc = (scripts / 'generator/common.uc').read_text(encoding='utf-8')
homeproxy_uc = (scripts / 'homeproxy-pro.uc').read_text(encoding='utf-8')
client_uc = (scripts / 'generate_client.uc').read_text(encoding='utf-8')
service_sh = (scripts / 'runtime/service.sh').read_text(encoding='utf-8')
update_sh = (scripts / 'update_resources.sh').read_text(encoding='utf-8')
fw_ut = (scripts / 'firewall_post.ut').read_text(encoding='utf-8')
problems = []
def strip_comments(src):
src = re.sub(r'/\*[\s\S]*?\*/', '', src)
return '\n'.join(l for l in src.split('\n') if not l.lstrip().startswith(('#', '//', '*')))
# (a) The default, in both places it is stated. Same shape as guard 39.
if "ruleset_safe_start: dm.general.ruleset_safe_start || '0'" not in context_uc:
problems.append("context.uc no longer falls back to '0' for ruleset_safe_start; a missing "
"UCI value would have to mean something other than the safe default")
if not re.search(r"^\toption ruleset_safe_start '0'$", config_uc, re.M):
problems.append("the package-shipped /etc/config/homeproxy-pro no longer ships ruleset_safe_start "
"'0'; upgrading users would silently start routing with empty rule-sets")
route, dns = strip_comments(route_uc), strip_comments(dns_uc)
# (b) An initial_path may only be emitted for a fallback that exists. Both
# sites, because one without the other is the silent failure.
if "ctx.ruleset_initial" not in strip_comments(route_uc):
problems.append('route.uc emits initial_path without checking ctx.ruleset_initial; a built-in '
'could be pointed at a file the CLI never wrote')
if "ctx.ruleset_initial" not in strip_comments(ruleset_uc):
problems.append('ruleset.uc emits initial_path without checking ctx.ruleset_initial')
if 'ruleset_initial' not in client_uc:
problems.append('generate_client.uc no longer writes the fallbacks, so ctx.ruleset_initial is '
'always empty and the opt-in does nothing')
# (a) None of the three may be a remote rule-set again. A `type: 'remote'`
# in the built-in block brings back both the cold-start download and the
# second copy of the data.
decl = re.search(r'if \(declaresBuiltinRuleSets.*?\n\t\}', route, re.S)
if not decl:
problems.append('route.uc no longer declares the built-in rule-sets behind '
'declaresBuiltinRuleSets(); the China split would have no data source')
else:
block = decl.group(0)
for tag in ('china-ip', 'china-ip6', 'china-domain'):
if "tag: '%s'" % tag not in block:
problems.append("route.uc no longer declares '%s'; the rule that matches it "
"would fail the whole configuration with rule-set not found" % tag)
if "type: 'remote'" in block:
problems.append("route.uc declares a built-in as type: 'remote' again; a cold start "
"would have to download it before the inbounds bind")
# (c) The built-ins come from the shared list. route.uc hard-coding the tags
# is how the two readers drifted in the first place: the CLI had no idea
# these rule-sets existed.
for tag in ('geoip-cn', 'geosite-cn'):
if re.search(r"tag:\s*'%s'" % re.escape(tag), strip_comments(route_uc)):
problems.append("route.uc hard-codes tag '%s' again; it must come from "
"BUILTIN_REMOTE_RULE_SETS so the CLI can create its fallback" % tag)
if 'BUILTIN_REMOTE_RULE_SETS' not in route_uc:
problems.append('route.uc no longer reads BUILTIN_REMOTE_RULE_SETS')
if 'BUILTIN_REMOTE_RULE_SETS' not in client_uc:
problems.append('generate_client.uc no longer reads BUILTIN_REMOTE_RULE_SETS')
# (b) The two halves must name the same tags. This is the part that actually
# drifted before: the route rule named geoip-cn and the DNS rule named
# geosite-cn, and the fix was a second rule rather than one list.
if re.search(r"rule_set:\s*'geoip-cn'", route + dns):
problems.append("a China rule names 'geoip-cn' again; the address list is china-ip, "
"generated from the same china_ip4.txt the nft set is rendered from")
if re.search(r"rule_set:\s*'geosite-cn'", route + dns):
problems.append("a China rule names 'geosite-cn' again; the domain list is china-domain, "
"generated from china_list.txt")
if "rule_set: 'china-domain'" not in dns:
problems.append('dns.uc no longer routes the China domain list; those domains would fall '
'through to the mode default and resolve through the proxy')
# (d) The shipped constant, byte for byte. It is a binary artifact in the
# tree, and a corrupted one fails in the worst possible way: sing-box
# cannot parse the initial file, ignores it, and blocks startup - the
# exact failure the file exists to prevent. Comparing the bytes here is
# the only thing standing between a bad `cp` and that.
expected_srs = bytes([0x53, 0x52, 0x53, 0x02, 0x78, 0xDA, 0x62, 0x00,
0x0C, 0x00, 0x00, 0x01, 0x00, 0x01])
srs = root / 'root/etc/homeproxy-pro/ruleset/initial/empty.srs'
src = root / 'root/etc/homeproxy-pro/ruleset/initial/empty.source.json'
if not srs.is_file():
problems.append('the offline fallback %s is missing from the package payload' % srs)
elif srs.read_bytes() != expected_srs:
problems.append('%s is %s, not the documented %s; it is the last-resort binary '
'fallback and a corrupted one makes sing-box block startup'
% (srs.name, srs.read_bytes().hex(' '), expected_srs.hex(' ')))
if not src.is_file():
problems.append('the empty source %s is missing from the package payload' % src)
elif src.read_bytes() != b'{"version":3,"rules":[]}':
problems.append('%s is %r, not the documented empty source' % (src.name, src.read_bytes()))
# (c) The fallback tags have to be declared under the same condition they are
# referenced, or sing-box rejects the config with an undeclared tag.
for name, src, tag in (('route', route, 'china-ip6'), ('dns', dns, 'china-ip6')):
if re.search(r"rule_set:\s*'china-ip6'", src) and 'china_ip6_ready' not in src:
problems.append('%s names china-ip6 without gating on china_ip6_ready; the declaration '
'is conditional, so this is an undeclared tag' % name)
# (e) The fallbacks live inside the rule-set archive, so the existing jail
# mount of HP_DIR and the existing path whitelist both already cover them.
# A directory outside the archive would need a whitelist change and a new
# jail mount, and neither exists.
m = re.search(r"RULESET_INITIAL_DIR\s*=\s*HP_DIR \+ '([^']*)'", homeproxy_uc)
if not m:
problems.append('RULESET_INITIAL_DIR is gone or no longer derived from HP_DIR')
elif not m.group(1).startswith('/ruleset/'):
problems.append('RULESET_INITIAL_DIR is %r, outside the rule-set archive; the jail mount and '
'the path whitelist would not cover it' % m.group(1))
if 'RULESET_INITIAL_DIR' not in common_uc and 'BUILTIN_REMOTE_RULE_SETS' not in common_uc:
problems.append('common.uc no longer carries the shared built-in list')
# (d) Both halves of each list have to be generated by something that runs.
# domain_ruleset.uc is the newest of the three and the only one with no
# long history pointing at it, so it is the one that can quietly go
# missing and leave china-domain.json absent - which degrades the DNS
# split with nothing in the log.
for name, src in (('runtime/service.sh', service_sh), ('update_resources.sh', update_sh)):
if 'domain_ruleset.uc' not in src:
problems.append('%s no longer invokes domain_ruleset.uc; china-domain.json is only '
'produced there, and a missing file silently drops the DNS half of '
'the split' % name)
if 'china_ip_ruleset.uc' not in service_sh:
problems.append('runtime/service.sh no longer invokes china_ip_ruleset.uc; a cold install '
'would have no china_ip4.json and the route half would be missing too')
# (e) The kernel's half has to keep coming from the same .txt the generator's
# half is made from. This is the pair that disagreed once.
if "resources_dir + '/china_ip4.txt'" not in fw_ut:
problems.append('firewall_post.ut no longer renders the mainland set from china_ip4.txt; '
'the kernel and the resolver would decide "mainland" from two lists again')
print('\n'.join(problems))
PY
)" || SAFE_START="__SCAN_FAILED__"
if [ "$SAFE_START" = "__SCAN_FAILED__" ]; then
fail "the safe-start scan could not run - fix the guard before trusting a pass"
elif [ -z "$SAFE_START" ]; then
pass "the startup fallback is opt-in, both sides read one shared list, and the"
pass " shipped constant is byte-for-byte the documented one"
)" || CN_LOCAL="__SCAN_FAILED__"
if [ "$CN_LOCAL" = "__SCAN_FAILED__" ]; then
fail "the China-split scan could not run - fix the guard before trusting a pass"
elif [ -z "$CN_LOCAL" ]; then
pass "the China split is three local files, the two halves name the same tags, and"
pass " each half is generated by something that runs on start and on update"
else
fail "the startup fallback has been broken or silently enabled:"
printf ' %s\n' "$SAFE_START"
fail "the China split has drifted off one list:"
printf ' %s\n' "$CN_LOCAL"
fi
echo
echo "== guard 53: every dm.general field the generators read is one the Loader lists =="
@@ -2748,7 +2745,7 @@ echo "== guard 53: every dm.general field the generators read is one the Loader
# left the option off.
#
# That is not a hypothetical. ruleset_safe_start was added to
# /etc/config/homeproxy-pro and read in context.uc, and nothing said the key was
# /etc/config/homeproxy-pro and read in context.uc (removed in r41), and nothing said the key was
# missing: the configuration generated cleanly, `sing-box check` passed, every
# test that did not look for the feature's effect was green, and the feature
# was simply never on. Only the e2e case that asserts an initial_path exists
@@ -2888,6 +2885,127 @@ else
printf ' %s\n' "$FETCH_LAYER"
fi
# guard 55: every join() call passes the separator first.
#
# ucode's join(sep, list) does not validate its arguments. Swapped, it
# returns null rather than raising, and null + '\n' is the string "null\n" -
# a non-empty file naming nothing. That shipped in r41: the node-address
# export wrote "null", dnsmasq rendered nftset=/.null/... and the node
# bypass silently did nothing while every existence check stayed green.
#
# This is cheap to pin because every call is a literal, so an argument that
# is a list literal or a call returning one is the tell.
join_bad="$(python3 - "$SCRIPTS" "$RPC" <<'PY_JOIN'
import re, sys, pathlib
def strip_comments(src):
src = re.sub(r"/\*.*?\*/", " ", src, flags=re.S)
return re.sub(r"//[^\n]*", " ", src)
def top_level_first(args):
"""First argument, split on commas that are not inside quotes or parens -
the separator itself is routinely ', '."""
depth, j = 0, 0
while j < len(args):
c = args[j]
if c in "'\"":
k = j + 1
while k < len(args) and args[k] != c:
k += 1
j = k + 1; continue
if c in "([{":
depth += 1
elif c in ")]}":
depth -= 1
elif c == "," and depth == 0:
return args[:j].strip()
j += 1
return args.strip()
def join_args(src, i):
depth, j, out = 0, i, []
while j < len(src):
c = src[j]
if c in "'\"":
k = j + 1
while k < len(src) and src[k] != c:
k += 1
out.append(src[j:k + 1]); j = k + 1; continue
if c == "(":
depth += 1
if depth == 1:
j += 1; continue
elif c == ")":
depth -= 1
if depth == 0:
return "".join(out), j
out.append(c); j += 1
return None, j
bad = []
for root in sys.argv[1:]:
p = pathlib.Path(root)
files = ([p] if p.is_file()
else sorted(p.rglob("*.uc")) + sorted(p.rglob("*.ut")))
for f in files:
src = strip_comments(f.read_text(errors="replace"))
for m in re.finditer(r"\bjoin\s*\(", src):
args, _ = join_args(src, m.end() - 1)
if args is None:
continue
first = top_level_first(args)
# A separator is a string literal. Anything else in that slot -
# a call, a subscript, a bare identifier - is the swapped form,
# and ucode answers it with null instead of raising.
if not re.fullmatch(r"'[^']*'|\"[^\"]*\"", first):
bad.append("%s: join(%s, ...)" % (f.name, first))
print("\n".join(sorted(set(bad))))
PY_JOIN
)"
if [ -n "$join_bad" ]; then
fail "a join() call does not pass a string literal first (ucode takes join(sep, list), and"
fail "swapped it returns null silently instead of raising):"
printf ' %s\n' "$join_bad"
else
pass "every join() call passes a string-literal separator first; swapped arguments would"
pass " have returned null silently and shipped an empty-of-meaning export"
fi
# guard 56: no `{% set %}` tag assigns the result of a function call.
#
# utpl's set tag takes a plain expression and rejects a call outright:
# `{% set x = f(); %}` is a parse error. The damage is disproportionate to
# the typo, because a parse error kills the whole render - firewall_post.ut
# produces a zero-byte fw4_post.nft, fw4 has nothing new to load, and the
# health gate quietly keeps the previous ruleset. Nothing looks broken: the
# service is up, the proxy works, and the template change is simply absent.
# r41 shipped such a line and never rendered once; r43 fixed it.
#
# So the rule is mechanical and worth pinning: inside a set tag, no `(`.
set_call="$(python3 - "$SCRIPTS" <<'PY_SET'
import re, sys, pathlib
bad = []
for p in sorted(pathlib.Path(sys.argv[1]).rglob("*.ut")):
src = p.read_text(errors="replace")
# Comments quote the mistake on purpose; only real tags count.
src = re.sub(r"/\*.*?\*/", " ", src, flags=re.S)
for m in re.finditer(r"\{%-?\s*set\b([^%]*?)-?%\}", src, flags=re.S):
rhs = m.group(1)
# A call is any identifier immediately followed by "(".
if re.search(r"[A-Za-z_][A-Za-z0-9_.]*\s*\(", rhs):
bad.append("%s: {%% set%s%%}" % (p.name, rhs.strip()))
print("\n".join(bad))
PY_SET
)"
if [ -n "$set_call" ]; then
fail "a {% set %} tag assigns a function call; utpl rejects it, and the parse error"
fail "zeroes the whole render instead of failing visibly:"
printf ' %s\n' "$set_call"
else
pass "no {% set %} tag calls a function (utpl would reject it and the whole render"
pass " would silently collapse to an empty ruleset)"
fi
echo
printf '%s checks, %s failures\n' "$checks" "$([ "$FAILED" = 0 ] && echo 0 || echo 'nonzero')"
if [ "$FAILED" != 0 ]; then
@@ -1,94 +0,0 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# Regression fixture for config.ruleset_safe_start = '1' (issue #7 batch 2a).
#
# custom.uci with the opt-in on and two REMOTE rule-sets added, because those
# are the only ones the fallback applies to: a local rule-set already has its
# file on disk and is never fetched during initialization.
#
# It is a separate file rather than three sed variations of custom.uci for two
# reasons. The opt-in has to be in the `config homeproxy-pro 'routing'` section
# (dm.general.ruleset_safe_start), which is a different section from anything
# the variations were anchored on; and a multi-expression sed that both inserts
# an option and appends a section is a construct this suite had never run -
# the one-expression form is proven, the two-expression form is not, and a test
# whose own tooling is untested is how a case silently stops testing anything.
#
# The two rule-sets cover the two shapes the fallback has to get right:
#
# rs_remote one tag. initial_path is a concrete path and one file is
# written for it.
# rs_multi two tags. sing-box substitutes {tag} and opens EVERY resulting
# file, so initial_path must carry the placeholder and one file
# must be written per tag - a rule-set with a literal
# "{tag}.srs" on disk falls back to downloading and blocks
# startup, which is the exact failure the feature removes.
#
# __RULESET_DIR__ is substituted by tests/ucode/test_generators.sh with the
# per-run archive.
config homeproxy-pro 'config'
option routing_mode 'custom'
option proxy_mode 'redirect_tproxy'
option ipv6_support '0'
option log_level 'error'
option ruleset_safe_start '1'
config homeproxy-pro 'routing'
option default_outbound 'direct-out'
option default_outbound_dns 'default-dns'
config homeproxy-pro 'infra'
option mixed_port '5330'
option dns_port '5333'
option redirect_port '5331'
option self_mark '100'
# direct node: exercises the direct_overrides write path
config node 'n_direct'
option label 'direct-node'
option type 'direct'
option override_address '1.2.3.4'
option override_port '8443'
# routing node + rule: exercises get_direct_override() reading direct_overrides
config routing_node 'rn_direct'
option enabled '1'
option label 'rn-direct'
option node 'n_direct'
option domain_resolver 'default-dns'
option domain_strategy 'prefer_ipv4'
config routing_rule 'rr_direct'
option enabled '1'
option label 'rr-direct'
option outbound 'rn_direct'
option action 'route'
# The two remote rule-sets the fallback applies to. Neither has an
# initial_path of its own, which is the whole precondition: a rule-set that has
# one is left completely alone by the fallback.
config ruleset 'rs_remote'
option enabled '1'
option label 'remote-rs'
option type 'remote'
option format 'binary'
option url 'https://example.invalid/remote.srs'
config ruleset 'rs_multi'
option enabled '1'
option label 'multi-rs'
option type 'remote'
option format 'binary'
option url 'https://example.invalid/{tag}.srs'
list extra_tags 'alt'
# A disabled remote rule-set: the fallback must not create a file for it, since
# build_user_rulesets() skips it before it can ever reach the generated
# configuration.
config ruleset 'rs_off'
option enabled '0'
option label 'off-rs'
option type 'remote'
option format 'binary'
option url 'https://example.invalid/off.srs'
+271 -168
View File
@@ -9,6 +9,8 @@ ip route show default
ip -6 route show default
ifstatus wan
ucode -S etc/homeproxy-pro/scripts/generate_client.uc
chmod 600 etc/crontabs/root
init.d/cron restart
init.d/dnsmasq restart
ip link show singtun0
ip link set singtun0 up
@@ -42,6 +44,8 @@ procd_open_instance log-cleaner
procd_set_param command etc/homeproxy-pro/scripts/clean_log.sh
procd_set_param respawn
procd_close_instance
ucode -S etc/homeproxy-pro/scripts/runtime/domain_ruleset.uc etc/homeproxy-pro/resources/china_list.txt etc/homeproxy-pro/resources/china-domain.json
chown sing-box:sing-box etc/homeproxy-pro/resources/china-domain.json
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.json
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.log
chown sing-box:sing-box etc/homeproxy-pro/cache.db
@@ -74,6 +78,7 @@ sing-box version -n
log sing-box 1.14.0 started.
start rc=0
chmod 600 etc/crontabs/root
chmod 600 etc/crontabs/root
init.d/cron restart
config_load homeproxy-pro
ip rule del fwmark 101 table 100
@@ -130,6 +135,10 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
nft flush set inet fw4 homeproxy_node_addr_v4
nft delete set inet fw4 homeproxy_node_addr_v4
nft flush set inet fw4 homeproxy_node_addr_v6
nft delete set inet fw4 homeproxy_node_addr_v6
nft flush set inet fw4 homeproxy_routing_port
nft delete set inet fw4 homeproxy_routing_port
fw4 reload
@@ -162,6 +171,8 @@ ifstatus wan
ucode -S etc/homeproxy-pro/scripts/generate_client.uc
chmod 600 etc/crontabs/root
init.d/cron restart
chmod 600 etc/crontabs/root
init.d/cron restart
init.d/dnsmasq restart
ip rule del fwmark 101 table 100
ip rule add fwmark 101 table 100
@@ -219,6 +230,8 @@ procd_set_param respawn
procd_close_instance
log WARNING: IPv6 support is ON but etc/homeproxy-pro/resources/china_ip6.txt has 0 usable prefixes.
log WARNING: mainland IPv6 cannot be told apart from foreign IPv6, so IPv6 is passed through UNPROXIED (both sides of the split degrade). Run: etc/homeproxy-pro/scripts/update_resources.sh china_ip6
ucode -S etc/homeproxy-pro/scripts/runtime/domain_ruleset.uc etc/homeproxy-pro/resources/china_list.txt etc/homeproxy-pro/resources/china-domain.json
chown sing-box:sing-box etc/homeproxy-pro/resources/china-domain.json
chown sing-box:sing-box etc/homeproxy-pro/certs
chmod 700 etc/homeproxy-pro/certs
chown sing-box:sing-box etc/homeproxy-pro/certs/server_privatekey.pem
@@ -269,90 +282,6 @@ sing-box version -n
log sing-box 1.14.0 started.
start rc=0
chmod 600 etc/crontabs/root
init.d/cron restart
config_load homeproxy-pro
ip rule del fwmark 101 table 100
ip route del local 0.0.0.0/0 dev lo table 100
ip -6 rule del fwmark 101 table 100
ip -6 route del local ::/0 dev lo table 100
ip route del default dev singtun0 table 100
ip rule del fwmark 102 table 100
ip -6 route del default dev singtun0 table 100
ip -6 rule del fwmark 102 table 100
nft flush chain inet fw4 homeproxy_output_redir
nft delete chain inet fw4 homeproxy_output_redir
nft flush chain inet fw4 homeproxy_redirect
nft delete chain inet fw4 homeproxy_redirect
nft flush chain inet fw4 homeproxy_redirect_proxy
nft delete chain inet fw4 homeproxy_redirect_proxy
nft flush chain inet fw4 homeproxy_redirect_proxy_port
nft delete chain inet fw4 homeproxy_redirect_proxy_port
nft flush chain inet fw4 homeproxy_redirect_lanac
nft delete chain inet fw4 homeproxy_redirect_lanac
nft flush chain inet fw4 homeproxy_mangle_prerouting
nft delete chain inet fw4 homeproxy_mangle_prerouting
nft flush chain inet fw4 homeproxy_mangle_output
nft delete chain inet fw4 homeproxy_mangle_output
nft flush chain inet fw4 homeproxy_mangle_tproxy
nft delete chain inet fw4 homeproxy_mangle_tproxy
nft flush chain inet fw4 homeproxy_mangle_tproxy_port
nft delete chain inet fw4 homeproxy_mangle_tproxy_port
nft flush chain inet fw4 homeproxy_mangle_lanac
nft delete chain inet fw4 homeproxy_mangle_lanac
nft flush chain inet fw4 homeproxy_mangle_mark
nft delete chain inet fw4 homeproxy_mangle_mark
nft flush chain inet fw4 homeproxy_mangle_tun
nft delete chain inet fw4 homeproxy_mangle_tun
nft flush chain inet fw4 homeproxy_mangle_tun_mark
nft delete chain inet fw4 homeproxy_mangle_tun_mark
nft flush set inet fw4 homeproxy_local_addr_v4
nft delete set inet fw4 homeproxy_local_addr_v4
nft flush set inet fw4 homeproxy_local_addr_v6
nft delete set inet fw4 homeproxy_local_addr_v6
nft flush set inet fw4 homeproxy_gfw_list_v4
nft delete set inet fw4 homeproxy_gfw_list_v4
nft flush set inet fw4 homeproxy_gfw_list_v6
nft delete set inet fw4 homeproxy_gfw_list_v6
nft flush set inet fw4 homeproxy_mainland_addr_v4
nft delete set inet fw4 homeproxy_mainland_addr_v4
nft flush set inet fw4 homeproxy_mainland_addr_v6
nft delete set inet fw4 homeproxy_mainland_addr_v6
nft flush set inet fw4 homeproxy_wan_proxy_addr_v4
nft delete set inet fw4 homeproxy_wan_proxy_addr_v4
nft flush set inet fw4 homeproxy_wan_proxy_addr_v6
nft delete set inet fw4 homeproxy_wan_proxy_addr_v6
nft flush set inet fw4 homeproxy_wan_direct_addr_v4
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
nft flush set inet fw4 homeproxy_routing_port
nft delete set inet fw4 homeproxy_routing_port
fw4 reload
init.d/miniupnpd enabled
init.d/miniupnpd running
init.d/miniupnpd restart
init.d/dnsmasq restart
log Service stopped.
config_load homeproxy-pro
ip link set singtun0 down
ip tuntap del mode tun name singtun0
stop rc=0
file var/run/homeproxy-pro/fw4_forward.nft:
file var/run/homeproxy-pro/fw4_input.nft:
file var/run/homeproxy-pro/fw4_post.nft:
file var/run/homeproxy-pro/homeproxy-pro.log:
file var/run/homeproxy-pro/known-good/sing-box-c.json: {| "log": {},| "inbounds": [| {| "tag": "dns-in",| "listen_port": 5333| },| {| "tag": "mixed-in",| "listen_port": 5330| }| ],| "outbounds": []|}|
file var/run/homeproxy-pro/known-good/sing-box-s.json: {| "log": {},| "inbounds": [],| "outbounds": []|}|
file var/run/homeproxy-pro/known-good/uci-snapshot.txt: homeproxy-pro.config.main_node=n1|homeproxy-pro.config.main_udp_node=u1|homeproxy-pro.config.proxy_mode=redirect_tproxy|homeproxy-pro.config.routing_mode=gfwlist|homeproxy-pro.config.routing_port=|homeproxy-pro.config.ipv6_support=1|homeproxy-pro.config.china_dns_server=|homeproxy-pro.infra.self_mark=|homeproxy-pro.infra.redirect_port=|homeproxy-pro.infra.tproxy_port=|homeproxy-pro.infra.tproxy_mark=101|homeproxy-pro.infra.tun_mark=102|homeproxy-pro.infra.tun_name=singtun0|homeproxy-pro.infra.tun_addr4=|homeproxy-pro.infra.tun_addr6=|homeproxy-pro.infra.dns_redirect=|homeproxy-pro.infra.dns_port=5333|homeproxy-pro.infra.common_port=|homeproxy-pro.infra.table_mark=100|homeproxy-pro.routing.bypass_cn_traffic=|homeproxy-pro.routing.default_outbound=direct-out|homeproxy-pro.server.enabled=1|homeproxy-pro.control.lan_proxy_mode=|list homeproxy-pro.control.listen_interfaces=|list homeproxy-pro.control.lan_direct_ipv4_ips=|list homeproxy-pro.control.lan_direct_ipv6_ips=|list homeproxy-pro.control.lan_direct_mac_addrs=|list homeproxy-pro.control.lan_proxy_ipv4_ips=|list homeproxy-pro.control.lan_proxy_ipv6_ips=|list homeproxy-pro.control.lan_proxy_mac_addrs=|list homeproxy-pro.control.lan_gaming_mode_ipv4_ips=|list homeproxy-pro.control.lan_gaming_mode_ipv6_ips=|list homeproxy-pro.control.lan_gaming_mode_mac_addrs=|list homeproxy-pro.control.lan_global_proxy_ipv4_ips=|list homeproxy-pro.control.lan_global_proxy_ipv6_ips=|list homeproxy-pro.control.lan_global_proxy_mac_addrs=|list homeproxy-pro.control.wan_proxy_ipv4_ips=|list homeproxy-pro.control.wan_proxy_ipv6_ips=|list homeproxy-pro.control.wan_direct_ipv4_ips=|list homeproxy-pro.control.wan_direct_ipv6_ips=|
===== scenario: C-nothing-configured (start) =====
uci -q show dhcp.@dnsmasq[0]
config_load homeproxy-pro
ucode -e import * as fs from 'fs'; exit('mkdtemp' in keys(fs) ? 0 : 1)
sing-box version -n
log Neither client nor server is configured. Service not started.
config_load homeproxy-pro
start rc=0
chmod 600 etc/crontabs/root
init.d/cron restart
config_load homeproxy-pro
@@ -410,6 +339,100 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
nft flush set inet fw4 homeproxy_node_addr_v4
nft delete set inet fw4 homeproxy_node_addr_v4
nft flush set inet fw4 homeproxy_node_addr_v6
nft delete set inet fw4 homeproxy_node_addr_v6
nft flush set inet fw4 homeproxy_routing_port
nft delete set inet fw4 homeproxy_routing_port
fw4 reload
init.d/miniupnpd enabled
init.d/miniupnpd running
init.d/miniupnpd restart
init.d/dnsmasq restart
log Service stopped.
config_load homeproxy-pro
ip link set singtun0 down
ip tuntap del mode tun name singtun0
stop rc=0
file var/run/homeproxy-pro/fw4_forward.nft:
file var/run/homeproxy-pro/fw4_input.nft:
file var/run/homeproxy-pro/fw4_post.nft:
file var/run/homeproxy-pro/homeproxy-pro.log:
file var/run/homeproxy-pro/known-good/sing-box-c.json: {| "log": {},| "inbounds": [| {| "tag": "dns-in",| "listen_port": 5333| },| {| "tag": "mixed-in",| "listen_port": 5330| }| ],| "outbounds": []|}|
file var/run/homeproxy-pro/known-good/sing-box-s.json: {| "log": {},| "inbounds": [],| "outbounds": []|}|
file var/run/homeproxy-pro/known-good/uci-snapshot.txt: homeproxy-pro.config.main_node=n1|homeproxy-pro.config.main_udp_node=u1|homeproxy-pro.config.proxy_mode=redirect_tproxy|homeproxy-pro.config.routing_mode=gfwlist|homeproxy-pro.config.routing_port=|homeproxy-pro.config.ipv6_support=1|homeproxy-pro.config.china_dns_server=|homeproxy-pro.infra.self_mark=|homeproxy-pro.infra.redirect_port=|homeproxy-pro.infra.tproxy_port=|homeproxy-pro.infra.tproxy_mark=101|homeproxy-pro.infra.tun_mark=102|homeproxy-pro.infra.tun_name=singtun0|homeproxy-pro.infra.tun_addr4=|homeproxy-pro.infra.tun_addr6=|homeproxy-pro.infra.dns_redirect=|homeproxy-pro.infra.dns_port=5333|homeproxy-pro.infra.common_port=|homeproxy-pro.infra.table_mark=100|homeproxy-pro.routing.bypass_cn_traffic=|homeproxy-pro.routing.default_outbound=direct-out|homeproxy-pro.server.enabled=1|homeproxy-pro.control.lan_proxy_mode=|list homeproxy-pro.control.listen_interfaces=|list homeproxy-pro.control.lan_direct_ipv4_ips=|list homeproxy-pro.control.lan_direct_ipv6_ips=|list homeproxy-pro.control.lan_direct_mac_addrs=|list homeproxy-pro.control.lan_proxy_ipv4_ips=|list homeproxy-pro.control.lan_proxy_ipv6_ips=|list homeproxy-pro.control.lan_proxy_mac_addrs=|list homeproxy-pro.control.lan_gaming_mode_ipv4_ips=|list homeproxy-pro.control.lan_gaming_mode_ipv6_ips=|list homeproxy-pro.control.lan_gaming_mode_mac_addrs=|list homeproxy-pro.control.lan_global_proxy_ipv4_ips=|list homeproxy-pro.control.lan_global_proxy_ipv6_ips=|list homeproxy-pro.control.lan_global_proxy_mac_addrs=|list homeproxy-pro.control.wan_proxy_ipv4_ips=|list homeproxy-pro.control.wan_proxy_ipv6_ips=|list homeproxy-pro.control.wan_direct_ipv4_ips=|list homeproxy-pro.control.wan_direct_ipv6_ips=|
===== scenario: C-nothing-configured (start) =====
uci -q show dhcp.@dnsmasq[0]
config_load homeproxy-pro
ucode -e import * as fs from 'fs'; exit('mkdtemp' in keys(fs) ? 0 : 1)
sing-box version -n
log Neither client nor server is configured. Service not started.
config_load homeproxy-pro
start rc=0
chmod 600 etc/crontabs/root
chmod 600 etc/crontabs/root
init.d/cron restart
config_load homeproxy-pro
ip rule del fwmark 101 table 100
ip route del local 0.0.0.0/0 dev lo table 100
ip -6 rule del fwmark 101 table 100
ip -6 route del local ::/0 dev lo table 100
ip route del default dev singtun0 table 100
ip rule del fwmark 102 table 100
ip -6 route del default dev singtun0 table 100
ip -6 rule del fwmark 102 table 100
nft flush chain inet fw4 homeproxy_output_redir
nft delete chain inet fw4 homeproxy_output_redir
nft flush chain inet fw4 homeproxy_redirect
nft delete chain inet fw4 homeproxy_redirect
nft flush chain inet fw4 homeproxy_redirect_proxy
nft delete chain inet fw4 homeproxy_redirect_proxy
nft flush chain inet fw4 homeproxy_redirect_proxy_port
nft delete chain inet fw4 homeproxy_redirect_proxy_port
nft flush chain inet fw4 homeproxy_redirect_lanac
nft delete chain inet fw4 homeproxy_redirect_lanac
nft flush chain inet fw4 homeproxy_mangle_prerouting
nft delete chain inet fw4 homeproxy_mangle_prerouting
nft flush chain inet fw4 homeproxy_mangle_output
nft delete chain inet fw4 homeproxy_mangle_output
nft flush chain inet fw4 homeproxy_mangle_tproxy
nft delete chain inet fw4 homeproxy_mangle_tproxy
nft flush chain inet fw4 homeproxy_mangle_tproxy_port
nft delete chain inet fw4 homeproxy_mangle_tproxy_port
nft flush chain inet fw4 homeproxy_mangle_lanac
nft delete chain inet fw4 homeproxy_mangle_lanac
nft flush chain inet fw4 homeproxy_mangle_mark
nft delete chain inet fw4 homeproxy_mangle_mark
nft flush chain inet fw4 homeproxy_mangle_tun
nft delete chain inet fw4 homeproxy_mangle_tun
nft flush chain inet fw4 homeproxy_mangle_tun_mark
nft delete chain inet fw4 homeproxy_mangle_tun_mark
nft flush set inet fw4 homeproxy_local_addr_v4
nft delete set inet fw4 homeproxy_local_addr_v4
nft flush set inet fw4 homeproxy_local_addr_v6
nft delete set inet fw4 homeproxy_local_addr_v6
nft flush set inet fw4 homeproxy_gfw_list_v4
nft delete set inet fw4 homeproxy_gfw_list_v4
nft flush set inet fw4 homeproxy_gfw_list_v6
nft delete set inet fw4 homeproxy_gfw_list_v6
nft flush set inet fw4 homeproxy_mainland_addr_v4
nft delete set inet fw4 homeproxy_mainland_addr_v4
nft flush set inet fw4 homeproxy_mainland_addr_v6
nft delete set inet fw4 homeproxy_mainland_addr_v6
nft flush set inet fw4 homeproxy_wan_proxy_addr_v4
nft delete set inet fw4 homeproxy_wan_proxy_addr_v4
nft flush set inet fw4 homeproxy_wan_proxy_addr_v6
nft delete set inet fw4 homeproxy_wan_proxy_addr_v6
nft flush set inet fw4 homeproxy_wan_direct_addr_v4
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
nft flush set inet fw4 homeproxy_node_addr_v4
nft delete set inet fw4 homeproxy_node_addr_v4
nft flush set inet fw4 homeproxy_node_addr_v6
nft delete set inet fw4 homeproxy_node_addr_v6
nft flush set inet fw4 homeproxy_routing_port
nft delete set inet fw4 homeproxy_routing_port
fw4 reload
@@ -435,6 +458,8 @@ ip route show default
ip -6 route show default
ifstatus wan
ucode -S etc/homeproxy-pro/scripts/generate_client.uc
chmod 600 etc/crontabs/root
init.d/cron restart
init.d/dnsmasq restart
ip link show singtun0
ip link set singtun0 up
@@ -468,6 +493,8 @@ procd_open_instance log-cleaner
procd_set_param command etc/homeproxy-pro/scripts/clean_log.sh
procd_set_param respawn
procd_close_instance
ucode -S etc/homeproxy-pro/scripts/runtime/domain_ruleset.uc etc/homeproxy-pro/resources/china_list.txt etc/homeproxy-pro/resources/china-domain.json
chown sing-box:sing-box etc/homeproxy-pro/resources/china-domain.json
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.json
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.log
chown sing-box:sing-box etc/homeproxy-pro/cache.db
@@ -504,6 +531,7 @@ config_load homeproxy-pro
ucode -S etc/homeproxy-pro/scripts/generate_client.uc
chmod 600 var/run/homeproxy-pro/candidate/sing-box-c.json
chmod 600 etc/crontabs/root
chmod 600 etc/crontabs/root
init.d/cron restart
config_load homeproxy-pro
nft flush chain inet fw4 homeproxy_output_redir
@@ -552,6 +580,10 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
nft flush set inet fw4 homeproxy_node_addr_v4
nft delete set inet fw4 homeproxy_node_addr_v4
nft flush set inet fw4 homeproxy_node_addr_v6
nft delete set inet fw4 homeproxy_node_addr_v6
nft flush set inet fw4 homeproxy_routing_port
nft delete set inet fw4 homeproxy_routing_port
fw4 reload
@@ -568,6 +600,8 @@ ip route show default
ip -6 route show default
ifstatus wan
log Activating the client configuration validated by the reload preflight.
chmod 600 etc/crontabs/root
init.d/cron restart
log dnsmasq snippets unchanged, skipping the restart.
ip link show singtun0
ip link set singtun0 up
@@ -601,6 +635,8 @@ procd_open_instance log-cleaner
procd_set_param command etc/homeproxy-pro/scripts/clean_log.sh
procd_set_param respawn
procd_close_instance
ucode -S etc/homeproxy-pro/scripts/runtime/domain_ruleset.uc etc/homeproxy-pro/resources/china_list.txt etc/homeproxy-pro/resources/china-domain.json
chown sing-box:sing-box etc/homeproxy-pro/resources/china-domain.json
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.json
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.log
chown sing-box:sing-box etc/homeproxy-pro/cache.db
@@ -756,6 +792,10 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
nft flush set inet fw4 homeproxy_node_addr_v4
nft delete set inet fw4 homeproxy_node_addr_v4
nft flush set inet fw4 homeproxy_node_addr_v6
nft delete set inet fw4 homeproxy_node_addr_v6
nft flush set inet fw4 homeproxy_routing_port
nft delete set inet fw4 homeproxy_routing_port
fw4 reload
@@ -806,6 +846,7 @@ uci delete homeproxy-pro.control.wan_direct_ipv4_ips
uci delete homeproxy-pro.control.wan_direct_ipv6_ips
uci commit homeproxy-pro
chmod 600 etc/crontabs/root
chmod 600 etc/crontabs/root
init.d/cron restart
config_load homeproxy-pro
nft flush chain inet fw4 homeproxy_output_redir
@@ -854,6 +895,10 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
nft flush set inet fw4 homeproxy_node_addr_v4
nft delete set inet fw4 homeproxy_node_addr_v4
nft flush set inet fw4 homeproxy_node_addr_v6
nft delete set inet fw4 homeproxy_node_addr_v6
nft flush set inet fw4 homeproxy_routing_port
nft delete set inet fw4 homeproxy_routing_port
fw4 reload
@@ -870,6 +915,8 @@ ip route show default
ip -6 route show default
ifstatus wan
log Starting with the last known-good client configuration.
chmod 600 etc/crontabs/root
init.d/cron restart
init.d/dnsmasq restart
ip link show singtun0
ip link set singtun0 up
@@ -903,6 +950,8 @@ procd_open_instance log-cleaner
procd_set_param command etc/homeproxy-pro/scripts/clean_log.sh
procd_set_param respawn
procd_close_instance
ucode -S etc/homeproxy-pro/scripts/runtime/domain_ruleset.uc etc/homeproxy-pro/resources/china_list.txt etc/homeproxy-pro/resources/china-domain.json
chown sing-box:sing-box etc/homeproxy-pro/resources/china-domain.json
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.json
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.log
chown sing-box:sing-box etc/homeproxy-pro/cache.db
@@ -949,6 +998,7 @@ log sing-box 1.14.0 started.
log Rollback completed; the previous configuration is running.
reload_service rc=0
chmod 600 etc/crontabs/root
chmod 600 etc/crontabs/root
init.d/cron restart
config_load homeproxy-pro
nft flush chain inet fw4 homeproxy_output_redir
@@ -997,6 +1047,10 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
nft flush set inet fw4 homeproxy_node_addr_v4
nft delete set inet fw4 homeproxy_node_addr_v4
nft flush set inet fw4 homeproxy_node_addr_v6
nft delete set inet fw4 homeproxy_node_addr_v6
nft flush set inet fw4 homeproxy_routing_port
nft delete set inet fw4 homeproxy_routing_port
fw4 reload
@@ -1027,6 +1081,8 @@ ip route show default
ip -6 route show default
ifstatus wan
ucode -S etc/homeproxy-pro/scripts/generate_client.uc
chmod 600 etc/crontabs/root
init.d/cron restart
init.d/dnsmasq restart
ip link show singtun0
ip link set singtun0 up
@@ -1060,6 +1116,8 @@ procd_open_instance log-cleaner
procd_set_param command etc/homeproxy-pro/scripts/clean_log.sh
procd_set_param respawn
procd_close_instance
ucode -S etc/homeproxy-pro/scripts/runtime/domain_ruleset.uc etc/homeproxy-pro/resources/china_list.txt etc/homeproxy-pro/resources/china-domain.json
chown sing-box:sing-box etc/homeproxy-pro/resources/china-domain.json
chown -R sing-box:sing-box etc/homeproxy-pro/ruleset
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.json
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.log
@@ -1093,90 +1151,6 @@ sing-box version -n
log sing-box 1.14.0 started.
start rc=0
chmod 600 etc/crontabs/root
init.d/cron restart
config_load homeproxy-pro
ip rule del fwmark 101 table 100
ip route del local 0.0.0.0/0 dev lo table 100
ip -6 rule del fwmark 101 table 100
ip -6 route del local ::/0 dev lo table 100
ip route del default dev singtun0 table 100
ip rule del fwmark 102 table 100
ip -6 route del default dev singtun0 table 100
ip -6 rule del fwmark 102 table 100
nft flush chain inet fw4 homeproxy_output_redir
nft delete chain inet fw4 homeproxy_output_redir
nft flush chain inet fw4 homeproxy_redirect
nft delete chain inet fw4 homeproxy_redirect
nft flush chain inet fw4 homeproxy_redirect_proxy
nft delete chain inet fw4 homeproxy_redirect_proxy
nft flush chain inet fw4 homeproxy_redirect_proxy_port
nft delete chain inet fw4 homeproxy_redirect_proxy_port
nft flush chain inet fw4 homeproxy_redirect_lanac
nft delete chain inet fw4 homeproxy_redirect_lanac
nft flush chain inet fw4 homeproxy_mangle_prerouting
nft delete chain inet fw4 homeproxy_mangle_prerouting
nft flush chain inet fw4 homeproxy_mangle_output
nft delete chain inet fw4 homeproxy_mangle_output
nft flush chain inet fw4 homeproxy_mangle_tproxy
nft delete chain inet fw4 homeproxy_mangle_tproxy
nft flush chain inet fw4 homeproxy_mangle_tproxy_port
nft delete chain inet fw4 homeproxy_mangle_tproxy_port
nft flush chain inet fw4 homeproxy_mangle_lanac
nft delete chain inet fw4 homeproxy_mangle_lanac
nft flush chain inet fw4 homeproxy_mangle_mark
nft delete chain inet fw4 homeproxy_mangle_mark
nft flush chain inet fw4 homeproxy_mangle_tun
nft delete chain inet fw4 homeproxy_mangle_tun
nft flush chain inet fw4 homeproxy_mangle_tun_mark
nft delete chain inet fw4 homeproxy_mangle_tun_mark
nft flush set inet fw4 homeproxy_local_addr_v4
nft delete set inet fw4 homeproxy_local_addr_v4
nft flush set inet fw4 homeproxy_local_addr_v6
nft delete set inet fw4 homeproxy_local_addr_v6
nft flush set inet fw4 homeproxy_gfw_list_v4
nft delete set inet fw4 homeproxy_gfw_list_v4
nft flush set inet fw4 homeproxy_gfw_list_v6
nft delete set inet fw4 homeproxy_gfw_list_v6
nft flush set inet fw4 homeproxy_mainland_addr_v4
nft delete set inet fw4 homeproxy_mainland_addr_v4
nft flush set inet fw4 homeproxy_mainland_addr_v6
nft delete set inet fw4 homeproxy_mainland_addr_v6
nft flush set inet fw4 homeproxy_wan_proxy_addr_v4
nft delete set inet fw4 homeproxy_wan_proxy_addr_v4
nft flush set inet fw4 homeproxy_wan_proxy_addr_v6
nft delete set inet fw4 homeproxy_wan_proxy_addr_v6
nft flush set inet fw4 homeproxy_wan_direct_addr_v4
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
nft flush set inet fw4 homeproxy_routing_port
nft delete set inet fw4 homeproxy_routing_port
fw4 reload
init.d/miniupnpd enabled
init.d/miniupnpd running
init.d/miniupnpd restart
init.d/dnsmasq restart
log Service stopped.
config_load homeproxy-pro
ip link set singtun0 down
ip tuntap del mode tun name singtun0
stop rc=0
file var/run/homeproxy-pro/fw4_forward.nft:
file var/run/homeproxy-pro/fw4_input.nft:
file var/run/homeproxy-pro/fw4_post.nft:
file var/run/homeproxy-pro/homeproxy-pro.log:
file var/run/homeproxy-pro/known-good/sing-box-c.json: {| "log": {},| "inbounds": [| {| "tag": "dns-in",| "listen_port": 5333| },| {| "tag": "mixed-in",| "listen_port": 5330| }| ],| "outbounds": []|}|
file var/run/homeproxy-pro/known-good/uci-snapshot.txt: homeproxy-pro.config.main_node=n1|homeproxy-pro.config.main_udp_node=nil|homeproxy-pro.config.proxy_mode=tun|homeproxy-pro.config.routing_mode=custom|homeproxy-pro.config.routing_port=|homeproxy-pro.config.ipv6_support=0|homeproxy-pro.config.china_dns_server=|homeproxy-pro.infra.self_mark=|homeproxy-pro.infra.redirect_port=|homeproxy-pro.infra.tproxy_port=|homeproxy-pro.infra.tproxy_mark=101|homeproxy-pro.infra.tun_mark=102|homeproxy-pro.infra.tun_name=singtun0|homeproxy-pro.infra.tun_addr4=|homeproxy-pro.infra.tun_addr6=|homeproxy-pro.infra.dns_redirect=|homeproxy-pro.infra.dns_port=5333|homeproxy-pro.infra.common_port=|homeproxy-pro.infra.table_mark=100|homeproxy-pro.routing.bypass_cn_traffic=|homeproxy-pro.routing.default_outbound=direct-out|homeproxy-pro.server.enabled=0|homeproxy-pro.control.lan_proxy_mode=|list homeproxy-pro.control.listen_interfaces=|list homeproxy-pro.control.lan_direct_ipv4_ips=|list homeproxy-pro.control.lan_direct_ipv6_ips=|list homeproxy-pro.control.lan_direct_mac_addrs=|list homeproxy-pro.control.lan_proxy_ipv4_ips=|list homeproxy-pro.control.lan_proxy_ipv6_ips=|list homeproxy-pro.control.lan_proxy_mac_addrs=|list homeproxy-pro.control.lan_gaming_mode_ipv4_ips=|list homeproxy-pro.control.lan_gaming_mode_ipv6_ips=|list homeproxy-pro.control.lan_gaming_mode_mac_addrs=|list homeproxy-pro.control.lan_global_proxy_ipv4_ips=|list homeproxy-pro.control.lan_global_proxy_ipv6_ips=|list homeproxy-pro.control.lan_global_proxy_mac_addrs=|list homeproxy-pro.control.wan_proxy_ipv4_ips=|list homeproxy-pro.control.wan_proxy_ipv6_ips=|list homeproxy-pro.control.wan_direct_ipv4_ips=|list homeproxy-pro.control.wan_direct_ipv6_ips=|
file etc/homeproxy-pro/cache.db:
===== scenario: F-custom-routing-nothing-configured (start) =====
uci -q show dhcp.@dnsmasq[0]
config_load homeproxy-pro
ucode -e import * as fs from 'fs'; exit('mkdtemp' in keys(fs) ? 0 : 1)
sing-box version -n
log Neither client nor server is configured. Service not started.
config_load homeproxy-pro
start rc=0
chmod 600 etc/crontabs/root
init.d/cron restart
config_load homeproxy-pro
@@ -1234,6 +1208,100 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
nft flush set inet fw4 homeproxy_node_addr_v4
nft delete set inet fw4 homeproxy_node_addr_v4
nft flush set inet fw4 homeproxy_node_addr_v6
nft delete set inet fw4 homeproxy_node_addr_v6
nft flush set inet fw4 homeproxy_routing_port
nft delete set inet fw4 homeproxy_routing_port
fw4 reload
init.d/miniupnpd enabled
init.d/miniupnpd running
init.d/miniupnpd restart
init.d/dnsmasq restart
log Service stopped.
config_load homeproxy-pro
ip link set singtun0 down
ip tuntap del mode tun name singtun0
stop rc=0
file var/run/homeproxy-pro/fw4_forward.nft:
file var/run/homeproxy-pro/fw4_input.nft:
file var/run/homeproxy-pro/fw4_post.nft:
file var/run/homeproxy-pro/homeproxy-pro.log:
file var/run/homeproxy-pro/known-good/sing-box-c.json: {| "log": {},| "inbounds": [| {| "tag": "dns-in",| "listen_port": 5333| },| {| "tag": "mixed-in",| "listen_port": 5330| }| ],| "outbounds": []|}|
file var/run/homeproxy-pro/known-good/uci-snapshot.txt: homeproxy-pro.config.main_node=n1|homeproxy-pro.config.main_udp_node=nil|homeproxy-pro.config.proxy_mode=tun|homeproxy-pro.config.routing_mode=custom|homeproxy-pro.config.routing_port=|homeproxy-pro.config.ipv6_support=0|homeproxy-pro.config.china_dns_server=|homeproxy-pro.infra.self_mark=|homeproxy-pro.infra.redirect_port=|homeproxy-pro.infra.tproxy_port=|homeproxy-pro.infra.tproxy_mark=101|homeproxy-pro.infra.tun_mark=102|homeproxy-pro.infra.tun_name=singtun0|homeproxy-pro.infra.tun_addr4=|homeproxy-pro.infra.tun_addr6=|homeproxy-pro.infra.dns_redirect=|homeproxy-pro.infra.dns_port=5333|homeproxy-pro.infra.common_port=|homeproxy-pro.infra.table_mark=100|homeproxy-pro.routing.bypass_cn_traffic=|homeproxy-pro.routing.default_outbound=direct-out|homeproxy-pro.server.enabled=0|homeproxy-pro.control.lan_proxy_mode=|list homeproxy-pro.control.listen_interfaces=|list homeproxy-pro.control.lan_direct_ipv4_ips=|list homeproxy-pro.control.lan_direct_ipv6_ips=|list homeproxy-pro.control.lan_direct_mac_addrs=|list homeproxy-pro.control.lan_proxy_ipv4_ips=|list homeproxy-pro.control.lan_proxy_ipv6_ips=|list homeproxy-pro.control.lan_proxy_mac_addrs=|list homeproxy-pro.control.lan_gaming_mode_ipv4_ips=|list homeproxy-pro.control.lan_gaming_mode_ipv6_ips=|list homeproxy-pro.control.lan_gaming_mode_mac_addrs=|list homeproxy-pro.control.lan_global_proxy_ipv4_ips=|list homeproxy-pro.control.lan_global_proxy_ipv6_ips=|list homeproxy-pro.control.lan_global_proxy_mac_addrs=|list homeproxy-pro.control.wan_proxy_ipv4_ips=|list homeproxy-pro.control.wan_proxy_ipv6_ips=|list homeproxy-pro.control.wan_direct_ipv4_ips=|list homeproxy-pro.control.wan_direct_ipv6_ips=|
file etc/homeproxy-pro/cache.db:
===== scenario: F-custom-routing-nothing-configured (start) =====
uci -q show dhcp.@dnsmasq[0]
config_load homeproxy-pro
ucode -e import * as fs from 'fs'; exit('mkdtemp' in keys(fs) ? 0 : 1)
sing-box version -n
log Neither client nor server is configured. Service not started.
config_load homeproxy-pro
start rc=0
chmod 600 etc/crontabs/root
chmod 600 etc/crontabs/root
init.d/cron restart
config_load homeproxy-pro
ip rule del fwmark 101 table 100
ip route del local 0.0.0.0/0 dev lo table 100
ip -6 rule del fwmark 101 table 100
ip -6 route del local ::/0 dev lo table 100
ip route del default dev singtun0 table 100
ip rule del fwmark 102 table 100
ip -6 route del default dev singtun0 table 100
ip -6 rule del fwmark 102 table 100
nft flush chain inet fw4 homeproxy_output_redir
nft delete chain inet fw4 homeproxy_output_redir
nft flush chain inet fw4 homeproxy_redirect
nft delete chain inet fw4 homeproxy_redirect
nft flush chain inet fw4 homeproxy_redirect_proxy
nft delete chain inet fw4 homeproxy_redirect_proxy
nft flush chain inet fw4 homeproxy_redirect_proxy_port
nft delete chain inet fw4 homeproxy_redirect_proxy_port
nft flush chain inet fw4 homeproxy_redirect_lanac
nft delete chain inet fw4 homeproxy_redirect_lanac
nft flush chain inet fw4 homeproxy_mangle_prerouting
nft delete chain inet fw4 homeproxy_mangle_prerouting
nft flush chain inet fw4 homeproxy_mangle_output
nft delete chain inet fw4 homeproxy_mangle_output
nft flush chain inet fw4 homeproxy_mangle_tproxy
nft delete chain inet fw4 homeproxy_mangle_tproxy
nft flush chain inet fw4 homeproxy_mangle_tproxy_port
nft delete chain inet fw4 homeproxy_mangle_tproxy_port
nft flush chain inet fw4 homeproxy_mangle_lanac
nft delete chain inet fw4 homeproxy_mangle_lanac
nft flush chain inet fw4 homeproxy_mangle_mark
nft delete chain inet fw4 homeproxy_mangle_mark
nft flush chain inet fw4 homeproxy_mangle_tun
nft delete chain inet fw4 homeproxy_mangle_tun
nft flush chain inet fw4 homeproxy_mangle_tun_mark
nft delete chain inet fw4 homeproxy_mangle_tun_mark
nft flush set inet fw4 homeproxy_local_addr_v4
nft delete set inet fw4 homeproxy_local_addr_v4
nft flush set inet fw4 homeproxy_local_addr_v6
nft delete set inet fw4 homeproxy_local_addr_v6
nft flush set inet fw4 homeproxy_gfw_list_v4
nft delete set inet fw4 homeproxy_gfw_list_v4
nft flush set inet fw4 homeproxy_gfw_list_v6
nft delete set inet fw4 homeproxy_gfw_list_v6
nft flush set inet fw4 homeproxy_mainland_addr_v4
nft delete set inet fw4 homeproxy_mainland_addr_v4
nft flush set inet fw4 homeproxy_mainland_addr_v6
nft delete set inet fw4 homeproxy_mainland_addr_v6
nft flush set inet fw4 homeproxy_wan_proxy_addr_v4
nft delete set inet fw4 homeproxy_wan_proxy_addr_v4
nft flush set inet fw4 homeproxy_wan_proxy_addr_v6
nft delete set inet fw4 homeproxy_wan_proxy_addr_v6
nft flush set inet fw4 homeproxy_wan_direct_addr_v4
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
nft flush set inet fw4 homeproxy_node_addr_v4
nft delete set inet fw4 homeproxy_node_addr_v4
nft flush set inet fw4 homeproxy_node_addr_v6
nft delete set inet fw4 homeproxy_node_addr_v6
nft flush set inet fw4 homeproxy_routing_port
nft delete set inet fw4 homeproxy_routing_port
fw4 reload
@@ -1259,6 +1327,8 @@ ip route show default
ip -6 route show default
ifstatus wan
ucode -S etc/homeproxy-pro/scripts/generate_client.uc
chmod 600 etc/crontabs/root
init.d/cron restart
init.d/dnsmasq restart
procd_open_instance sing-box-c
procd_set_param command /usr/bin/sing-box
@@ -1287,6 +1357,8 @@ procd_open_instance log-cleaner
procd_set_param command etc/homeproxy-pro/scripts/clean_log.sh
procd_set_param respawn
procd_close_instance
ucode -S etc/homeproxy-pro/scripts/runtime/domain_ruleset.uc etc/homeproxy-pro/resources/china_list.txt etc/homeproxy-pro/resources/china-domain.json
chown sing-box:sing-box etc/homeproxy-pro/resources/china-domain.json
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.json
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.log
chown sing-box:sing-box etc/homeproxy-pro/cache.db
@@ -1442,6 +1514,10 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
nft flush set inet fw4 homeproxy_node_addr_v4
nft delete set inet fw4 homeproxy_node_addr_v4
nft flush set inet fw4 homeproxy_node_addr_v6
nft delete set inet fw4 homeproxy_node_addr_v6
nft flush set inet fw4 homeproxy_routing_port
nft delete set inet fw4 homeproxy_routing_port
fw4 reload
@@ -1451,6 +1527,7 @@ init.d/miniupnpd restart
init.d/dnsmasq restart
start rc=1
chmod 600 etc/crontabs/root
chmod 600 etc/crontabs/root
init.d/cron restart
config_load homeproxy-pro
ip rule del fwmark 101 table 100
@@ -1507,6 +1584,10 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
nft flush set inet fw4 homeproxy_node_addr_v4
nft delete set inet fw4 homeproxy_node_addr_v4
nft flush set inet fw4 homeproxy_node_addr_v6
nft delete set inet fw4 homeproxy_node_addr_v6
nft flush set inet fw4 homeproxy_routing_port
nft delete set inet fw4 homeproxy_routing_port
fw4 reload
@@ -1534,6 +1615,8 @@ ip route show default
ip -6 route show default
ifstatus wan
ucode -S etc/homeproxy-pro/scripts/generate_client.uc
chmod 600 etc/crontabs/root
init.d/cron restart
log Warning: infra.dns_port=70000 is out of range; skipping the dnsmasq snippets.
log Error: the dnsmasq snippets were not installed; LAN DNS keeps using the previous configuration.
ip link show singtun0
@@ -1568,6 +1651,8 @@ procd_open_instance log-cleaner
procd_set_param command etc/homeproxy-pro/scripts/clean_log.sh
procd_set_param respawn
procd_close_instance
ucode -S etc/homeproxy-pro/scripts/runtime/domain_ruleset.uc etc/homeproxy-pro/resources/china_list.txt etc/homeproxy-pro/resources/china-domain.json
chown sing-box:sing-box etc/homeproxy-pro/resources/china-domain.json
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.json
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.log
chown sing-box:sing-box etc/homeproxy-pro/cache.db
@@ -1599,6 +1684,7 @@ sing-box version -n
log sing-box 1.14.0 started.
start rc=0
chmod 600 etc/crontabs/root
chmod 600 etc/crontabs/root
init.d/cron restart
config_load homeproxy-pro
ip rule del fwmark 101 table 100
@@ -1655,6 +1741,10 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
nft flush set inet fw4 homeproxy_node_addr_v4
nft delete set inet fw4 homeproxy_node_addr_v4
nft flush set inet fw4 homeproxy_node_addr_v6
nft delete set inet fw4 homeproxy_node_addr_v6
nft flush set inet fw4 homeproxy_routing_port
nft delete set inet fw4 homeproxy_routing_port
fw4 reload
@@ -1682,6 +1772,8 @@ ip route show default
ip -6 route show default
ifstatus wan
ucode -S etc/homeproxy-pro/scripts/generate_client.uc
chmod 600 etc/crontabs/root
init.d/cron restart
init.d/dnsmasq restart
procd_open_instance sing-box-c
procd_set_param command /usr/bin/sing-box
@@ -1731,6 +1823,8 @@ procd_open_instance log-cleaner
procd_set_param command etc/homeproxy-pro/scripts/clean_log.sh
procd_set_param respawn
procd_close_instance
ucode -S etc/homeproxy-pro/scripts/runtime/domain_ruleset.uc etc/homeproxy-pro/resources/china_list.txt etc/homeproxy-pro/resources/china-domain.json
chown sing-box:sing-box etc/homeproxy-pro/resources/china-domain.json
chown sing-box:sing-box etc/homeproxy-pro/certs
chmod 700 etc/homeproxy-pro/certs
chown sing-box:sing-box etc/homeproxy-pro/certs/server_privatekey.pem
@@ -1839,6 +1933,10 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
nft flush set inet fw4 homeproxy_node_addr_v4
nft delete set inet fw4 homeproxy_node_addr_v4
nft flush set inet fw4 homeproxy_node_addr_v6
nft delete set inet fw4 homeproxy_node_addr_v6
nft flush set inet fw4 homeproxy_routing_port
nft delete set inet fw4 homeproxy_routing_port
fw4 reload
@@ -1904,6 +2002,7 @@ sing-box version -n
log sing-box 1.14.0 started.
start rc=0
chmod 600 etc/crontabs/root
chmod 600 etc/crontabs/root
init.d/cron restart
config_load homeproxy-pro
ip rule del fwmark 101 table 100
@@ -1960,6 +2059,10 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
nft flush set inet fw4 homeproxy_node_addr_v4
nft delete set inet fw4 homeproxy_node_addr_v4
nft flush set inet fw4 homeproxy_node_addr_v6
nft delete set inet fw4 homeproxy_node_addr_v6
nft flush set inet fw4 homeproxy_routing_port
nft delete set inet fw4 homeproxy_routing_port
fw4 reload
@@ -178,10 +178,10 @@ function makePoll() {
/* --- the rule-set degradation report ------------------------------------
*
* parseRuleSetFetchFailures() is the only thing that tells a user a rule-set
* is currently EMPTY, because with ruleset_safe_start on there is no other
* symptom: the instance is healthy, the health gate passes (nothing in
* health.sh looks at rule-sets), and the LAN is proxied.
* A remote rule-set is fetched before the service starts, so a failure there
* is a failed start and the running instance has nothing to show for it. The
* reason is only ever written to a log, and a start that failed has no status
* page - so this parse is the only place it becomes legible without ssh.
*
* It parses a file sing-box is concurrently writing and that clean_log.sh
* truncates at 50 KB, so the cases below are about tolerance and about not
@@ -362,6 +362,12 @@ echo "== china ip rule-set generator =="
# 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
# The DNS half of the same split. Same failure mode and the same consequence
# class - one bad entry in a list that is replaced unattended makes sing-box
# reject the whole rule-set - plus one that is its own: the entry has to be a
# suffix, or the split quietly stops matching anything under the listed names.
sh "$ROOT/tests/ucode/test_domain_ruleset.sh" "$ROOT" "$WORK/domain_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
@@ -83,6 +83,17 @@ stage_case() {
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';#" \
@@ -173,15 +184,15 @@ check_fallback_pair() {
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
# 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": "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"
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
;;
@@ -232,9 +243,9 @@ check_proxy_list() {
# 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})"
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
;;
+184
View File
@@ -0,0 +1,184 @@
#!/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"
@@ -118,6 +118,86 @@ OUT="$STAGE/rendered.nft"
ERR="$STAGE/render.err"
trap '[ "$OWN_STAGE" = 1 ] && rm -rf "$STAGE"' EXIT INT TERM
# --- the node-address bypass is not gated on the literal-address file ------
#
# This is a source-level check, not a render one, on purpose. The render layer
# below is skipped wherever firewall4 is absent - which is every off-target
# run - and the failure this pins is one that renders perfectly and does
# nothing: with a host-name node there are no literal addresses, so a set and
# rules gated on "the literal list is non-empty" simply are not emitted, and
# dnsmasq's nftset= back-fill has no set to land in. r41 shipped that.
#
# The invariant, stated so it can be grepped: `node_addr_v4` may guard the
# `elements = { ... }` line and nothing else. Deciding whether the set and the
# return rules exist at all belongs to node_protect, which is true when either
# export file has content.
if ! grep -q 'function node_addr_protected(run_dir)' "$TEMPLATE_SRC"; then
echo "FAIL: firewall_post.ut has no node_addr_protected(); the node bypass cannot"
echo " distinguish 'nothing to protect' from 'a host name dnsmasq will resolve'"
FAILED=1
else
np_body="$(sed -n '/function node_addr_protected(run_dir)/,/^}/p' "$TEMPLATE_SRC")"
for f in node-addr-ips.txt node-addr-domains.txt; do
if ! printf '%s' "$np_body" | grep -q "$f"; then
echo "FAIL: node_addr_protected() does not look at $f"
FAILED=1
fi
done
[ "$FAILED" -eq 0 ] && echo "PASS: node_addr_protected() covers both export files"
fi
# Both helpers read run_dir, and they are defined ~80 lines above its
# declaration. In this ucode a closure captures the block variables that exist
# where the function is DEFINED, so a function defined earlier cannot see a
# const declared later - the call fails with "access to undeclared variable
# run_dir" while run_dir sits right there in the file. Passing it in is the
# only spelling that works, and r43 is what proved it.
for fn in node_addr_protected node_addr_to_nftarr; do
if ! grep -q "function $fn(run_dir" "$TEMPLATE_SRC"; then
echo "FAIL: $fn does not take run_dir as a parameter; in this ucode a closure only"
echo " sees block variables declared before the function definition, and both"
echo " helpers are defined above run_dir"
FAILED=1
fi
done
[ "$FAILED" -eq 0 ] && echo "PASS: the node-address helpers take run_dir as a parameter"
# Every consumer of the v4 set outside its own elements= line must be gated on
# node_protect. `node_addr_v4)` as a condition is exactly the mistake.
stray_gate="$(awk '
/if \(node_addr_v4/ {
# The one legitimate use guards the elements= line that follows it.
saw = 1; ln = NR; next
}
saw {
if ($0 ~ /elements *= *\{/) { saw = 0; next }
print " " ln ": " $0
saw = 0
}
' "$TEMPLATE_SRC" || true)"
if [ -n "$stray_gate" ]; then
echo "FAIL: the node-address set is gated on node_addr_v4 having elements; a host-name"
echo " node has none, and the set plus its return rules then never exist:"
printf ' %s\n' "$stray_gate"
FAILED=1
fi
# And the return rules themselves must be present in all three chains, each
# behind node_protect rather than behind a literal-address list.
for chain in redirect mangle_prerouting mangle_output; do
if ! grep -q "$chain" "$TEMPLATE_SRC"; then
echo "FAIL: chain $chain disappeared from firewall_post.ut"
FAILED=1
fi
done
v4_rules="$(grep -c 'ip daddr @homeproxy_node_addr_v4 counter return' "$TEMPLATE_SRC" || true)"
if [ "$v4_rules" -eq 3 ]; then
echo "PASS: all three chains carry the node-address return rule"
else
echo "FAIL: expected 3 'ip daddr @homeproxy_node_addr_v4 counter return' rules, found $v4_rules"
FAILED=1
fi
# Probe with ucode, not utpl: `utpl -e` is not an eval flag, it renders the
# argument as template text and always succeeds.
if ! ucode -e 'require("fw4");' > "/dev/null" 2>&1; then
@@ -73,6 +73,19 @@ run_case() {
fi
fi
# And the DNS half of the split, staged for the same reason and by the
# same route: hp_prepare_runtime_files runs domain_ruleset.uc over
# china_list.txt, and generate_client.uc reports the result as
# ctx.china_domain_ready (an lstat on this very file). Without it the
# DNS rule that carries the domestic lookup is not emitted at all - by
# design, because a rule_set naming a tag nothing declared makes
# sing-box reject the whole config.
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
if grep -q "__RULESET_DIR__" "$fixture"; then
# The fixture needs a real local rule-set on disk. The path has to
# satisfy validateRuleSetPath(), whose RULE_PATH_ROOTS was rewritten
@@ -127,15 +140,11 @@ run_case() {
fi
# Stage the two empty-rule-set files the "do not block startup" fallback
# uses. RULESET_INITIAL_DIR is derived from HP_DIR, which was rewritten
# to $dir above, so the generator looks for them under $dir/ruleset/initial
# and the repository's copy has to be staged there. Copied rather than
# referenced so a case that deletes or corrupts one cannot damage the
# checkout - the byte-exactness of the constant is arch-guard 52's job.
mkdir -p "$dir/ruleset/initial"
cp "$ROOT/root/etc/homeproxy-pro/ruleset/initial/empty.srs" "$dir/ruleset/initial/"
cp "$ROOT/root/etc/homeproxy-pro/ruleset/initial/empty.source.json" "$dir/ruleset/initial/"
# Stage the rule-set archive. A user-defined `type: local` rule-set names
# a path under it, and `sing-box check` opens every one of them, so the
# directory has to exist before the generated configuration is validated -
# the same reason hp_prepare_ruleset_dir() creates it on a real start.
mkdir -p "$dir/ruleset"
# HP_VALIDATE_DATA lets a development host replace /sbin/validate_data
# (see tests/README.md); on a target the production path is kept.
@@ -464,6 +473,75 @@ EOF
esac
fi
# The same fixture with a NON-default self_mark. 250 is deliberately clear
# of every other mark in the fixtures (self_mark 100, tproxy_mark 101,
# tun_mark 102), so a value that leaked in from the wrong place would show up
# as a mismatch rather than as a coincidence.
#
# Why this case exists at all: the mark is a two-sided contract read from one
# UCI option - firewall_post.ut renders `meta mark <self_mark> counter
# return`, generator/context.uc turns the same value into every outbound's
# routing_mark. Both sides read infra.self_mark, but they normalise it
# differently: the nft side goes through mark_value_or(), which falls back to
# 100 for anything fw4.parse_mark does not reduce to a bare hex value, while
# the generator hands the raw string to strToInt(). Until now every fixture
# declared 100, so the two paths never had to agree on anything else and the
# divergence was untested.
#
# A dedicated fixture file would differ from redirect.uci by that one option,
# which is what run_case's variation argument is for (see the main_udp_node
# case below for the other user of it). The expected value is read back from
# the STAGED fixture rather than hardcoded: fixture_self_mark() takes a path,
# and the staged copy is what the generator actually saw, so the assertion
# cannot drift away from its input the way a literal 250 would.
run_case redirect-self-mark "$ROOT/tests/fixtures/generators/redirect.uci" generate_client.uc sing-box-c.json \
"s/^\([[:space:]]*\)option self_mark '100'/\1option self_mark '250'/"
rsm_dir="$WORK/redirect-self-mark"
rsm_json="$rsm_dir/run/sing-box-c.json"
rsm_mark="$(fixture_self_mark "$rsm_dir/config/homeproxy-pro")"
if [ "$rsm_mark" != "250" ]; then
# Two ways to land here, both a vacuous assertion rather than a product
# failure: the variation did not apply, or fixture_self_mark cannot read
# what is there. run_case's own cmp guard catches a variation that
# matched nothing, so reaching this means the mark is unreadable.
echo "FAIL: redirect-self-mark: the staged fixture declares self_mark='$rsm_mark',"
echo " not the 250 this case varies to, so the assertion is vacuous"
FAILED=1
elif [ ! -f "$rsm_json" ]; then
echo "FAIL: redirect-self-mark: no config was generated"
FAILED=1
else
mark_result="$(ucode "$WORK/markcheck.uc" "$rsm_mark" "$rsm_json" 2>"/dev/null")"
mark_checked="${mark_result%% *}"
mark_problems="${mark_result##* }"
case "$mark_checked" in
''|*[!0-9]*)
echo "FAIL: redirect-self-mark: could not check the emitted outbounds ($rsm_json)"
FAILED=1
;;
0)
echo "FAIL: redirect-self-mark: no dialling outbound was emitted, the assertion is vacuous"
FAILED=1
;;
*)
if [ "$mark_problems" -ne 0 ]; then
echo "FAIL: redirect-self-mark: $mark_problems of $mark_checked dialling outbounds do not"
echo " carry routing_mark=$rsm_mark. With infra.self_mark at a non-default"
echo " value the nft side and the generator must still agree: a generator"
echo " that fell back to 100 (or dropped the field) while firewall_post.ut"
echo " emitted 'meta mark 250 counter return' leaves the loop-prevention"
echo " return matching nothing."
FAILED=1
else
echo "PASS: redirect-self-mark: all $mark_checked dialling outbounds carry routing_mark=$rsm_mark"
fi
;;
esac
fi
# The UDP tproxy path: a dedicated UDP node makes context.uc assign
# tproxy_port, firewall_post.ut emit the tproxy chain, and inbound.uc emit the
# tproxy-in that chain redirects to. No fixture covered this path before -
@@ -722,7 +800,7 @@ const config = json(readfile(ARGV[0]));
const rules = config.dns?.rules || [];
let has_eval_tag = false, has_match_response = false, geoip_ref = false;
/* rule_set is emitted as an array because the cn-fallback match names more
* than one rule-set when IPv6 support is on (geoip-cn + china-ip6). sing-box
* than one rule-set when IPv6 support is on (china-ip + china-ip6). sing-box
* accepts a bare string for the single-tag case, so accept both shapes here
* rather than pinning the one this build happens to produce. */
const names = (v) => (type(v) === 'array' ? v : [v]);
@@ -731,7 +809,7 @@ for (let r in rules) {
has_eval_tag = true;
if (r.match_response === 'cn-fallback') {
has_match_response = true;
if (index(names(r.rule_set), 'geoip-cn') >= 0)
if (index(names(r.rule_set), 'china-ip') >= 0)
geoip_ref = true;
}
}
@@ -739,7 +817,7 @@ printf('%d %d %d\n', +has_eval_tag, +has_match_response, +geoip_ref);
EOF
cn_info="$(ucode "$WORK/cn-fallback.uc" "$client_json" 2>/dev/null)"
if [ "$cn_info" = "1 1 1" ]; then
echo "PASS: cn-fallback-default-on: evaluate+match_response with geoip-cn is in the proxy config"
echo "PASS: cn-fallback-default-on: evaluate+match_response with china-ip is in the proxy config"
else
echo "FAIL: cn-fallback-default-on: evaluate/match_response flags = '$cn_info', expected '1 1 1'"
FAILED=1
@@ -957,36 +1035,36 @@ run_case_type_error dangling-resolver "does not exist or is disabled" \
# it runs identically on macOS and the testbed. ucode-stdout probes
# are out; structural assertions against the emitted bytes are in.
# 1) P2 #2: geosite-cn is the *DNS*-layer rule_set consumer.
# 1) china-domain is the *DNS*-layer rule_set consumer.
# The route layer never references it as a rule_set value (route.rule_set
# declares it as a tag). Indent cannot disambiguate dns.rules[] from
# route.rules[] (both pretty-print at 4 tabs on the testbed; both are
# bare-string `"rule_set": "..."`), so use count-based invariants:
# - "tag": "geosite-cn" appears at least once (route.rule_set)
# - "rule_set": "geosite-cn" appears EXACTLY once (dns.rules only;
# - "tag": "china-domain" appears at least once (route.rule_set)
# - "rule_set": "china-domain" appears EXACTLY once (dns.rules only;
# route.rules must NOT add a second one).
# A route regression that adds `"rule_set": "geosite-cn"` to route.rules
# A route regression that adds `"rule_set": "china-domain"` to route.rules
# bumps the count to 2 and fails loud.
run_case route-no-geosite-cn "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json
run_case route-no-china-domain "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json
rs_json="$WORK/route-no-geosite-cn/run/sing-box-c.json"
rs_json="$WORK/route-no-china-domain/run/sing-box-c.json"
if [ ! -f "$rs_json" ]; then
echo "FAIL: route-no-geosite-cn: no config was generated"
echo "FAIL: route-no-china-domain: no config was generated"
FAILED=1
else
rs_tag_count="$(grep -c '"tag": "geosite-cn"' "$rs_json")"
rs_ruleset_count="$(grep -c '"rule_set": "geosite-cn"' "$rs_json")"
rs_tag_count="$(grep -c '"tag": "china-domain"' "$rs_json")"
rs_ruleset_count="$(grep -c '"rule_set": "china-domain"' "$rs_json")"
if [ "$rs_tag_count" -lt 1 ]; then
echo "FAIL: route-no-geosite-cn: route.rule_set does not declare geosite-cn (tag count=$rs_tag_count, want >=1)"
echo "FAIL: route-no-china-domain: route.rule_set does not declare china-domain (tag count=$rs_tag_count, want >=1)"
FAILED=1
fi
if [ "$rs_ruleset_count" -ne 1 ]; then
echo "FAIL: route-no-geosite-cn: expected exactly 1 '\"rule_set\": \"geosite-cn\"' line (dns.rules only); got $rs_ruleset_count"
grep -n '"rule_set": "geosite-cn"' "$rs_json" | sed 's/^/ /'
echo "FAIL: route-no-china-domain: expected exactly 1 '\"rule_set\": \"china-domain\"' line (dns.rules only); got $rs_ruleset_count"
grep -n '"rule_set": "china-domain"' "$rs_json" | sed 's/^/ /'
FAILED=1
fi
if [ "$rs_tag_count" -ge 1 ] && [ "$rs_ruleset_count" -eq 1 ]; then
echo "PASS: route-no-geosite-cn: route.rule_set has geosite-cn (tag=$rs_tag_count); route.rules never references it (rule_set=$rs_ruleset_count, dns only)"
echo "PASS: route-no-china-domain: route.rule_set has china-domain (tag=$rs_tag_count); route.rules never references it (rule_set=$rs_ruleset_count, dns only)"
fi
fi
@@ -1355,177 +1433,74 @@ for pair in "ruleset-update-interval:3600s:3600" "ruleset-update-interval-unit:2
fi
done
# 8) Batch 2a: the "do not block startup on the first download" fallback.
# 8) Node server addresses: the generator exports them for the intercept layer.
#
# A remote rule-set with no initial_path is fetched during initialization,
# BEFORE the inbounds bind. On a cold cache - which is what a fresh install
# has, since cache.db ships empty - that means the first start waits on
# raw.githubusercontent.com, through the selected node, before the instance
# will listen at all; and a failure there is a FATAL that the health gate
# turns into a rollback or a released intercept layer.
# LAN traffic to the node's own address used to be redirected into sing-box,
# which then dialled that same address through the tunnel to that same node.
# sing-box's own outbound is exempt (routing_mark / self_mark); the LAN was
# not, and nothing in the rule-set kept it out.
#
# The opt-in points such a rule-set at an EMPTY rule-set, so sing-box reads
# a file, starts, and refreshes from the URL in the background. It is
# opt-in because an empty rule-set matches nothing and everything it would
# have split falls through to `final` - a routing change, not a robustness
# tweak.
#
# The first case is the one that matters most and is the easiest to get
# wrong: with the switch off, nothing may change. Not "the feature is off",
# but no initial_path, no file written, and a configuration that differs
# from the pre-feature one.
# The two files are the whole mechanism, and both are checked here because
# either one being wrong fails silently: a missing domains file means dnsmasq
# writes no nftset= snippet, and a missing ips file means the set renders with
# no elements. Neither logs anything at the point of failure.
run_case node-addr "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json
# 8a) The default. opt-in off -> no initial_path anywhere, and no fallback
# file created. The generated config must be identical to the same
# fixture with the option absent, which is the upgrade-invisible property.
run_case safe-start-off "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json
run_case safe-start-off-explicit "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json \
"s/option log_level 'error'/option log_level 'error'\n\toption ruleset_safe_start '0'/"
for name in safe-start-off safe-start-off-explicit; do
f="$WORK/$name/run/sing-box-c.json"
if [ ! -f "$f" ]; then
echo "FAIL: $name: no config was generated"
FAILED=1
elif grep -q 'initial_path' "$f"; then
echo "FAIL: $name: the opt-in is OFF but the config carries an initial_path:"
grep -n 'initial_path' "$f" | sed 's/^/ /'
FAILED=1
elif [ -d "$WORK/$name/ruleset/initial" ] && [ -n "$(ls -A "$WORK/$name/ruleset/initial" 2>/dev/null | grep -v '^empty\.')" ]; then
echo "FAIL: $name: the opt-in is OFF but a fallback file was written:"
ls -1 "$WORK/$name/ruleset/initial" | sed 's/^/ /'
FAILED=1
else
echo "PASS: $name: opt-in off -> no initial_path, no fallback file"
fi
done
# 8b) opt-in on, the built-ins. These two ARE the feature: an earlier version
# of this file only knew how to build their entries, and the CLI had no
# idea they existed, so a cold install blocked on downloading them.
run_case safe-start-on "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json \
"s/option log_level 'error'/option log_level 'error'\n\toption ruleset_safe_start '1'/"
ss_json="$WORK/safe-start-on/run/sing-box-c.json"
ss_initial="$WORK/safe-start-on/ruleset/initial"
if [ ! -f "$ss_json" ]; then
echo "FAIL: safe-start-on: no config was generated"
na_dir="$WORK/node-addr/run"
if [ ! -f "$na_dir/sing-box-c.json" ]; then
echo "FAIL: node-addr: no config was generated"
FAILED=1
else
# grep -F on the fully-constructed path, not an awk range around the entry.
# The range form over-captured: it ran past the entry's closing brace into
# the next one and then reported success on whatever it found, so a
# rule-set with NO initial_path would have passed whenever the next entry
# had one. Asserting the exact string also pins that the path is inside
# this run's archive, which is the property that makes it readable by the
# jailed client and admitted by the path whitelist.
for tag in geoip-cn geosite-cn; do
want="\"initial_path\": \"$ss_initial/$tag.srs\""
if ! grep -qF "$want" "$ss_json"; then
echo "FAIL: safe-start-on: the built-in '$tag' does not carry"
echo " $want"
grep -n 'initial_path' "$ss_json" | sed 's/^/ /'
FAILED=1
elif [ ! -s "$ss_initial/$tag.srs" ]; then
echo "FAIL: safe-start-on: '$tag' points at an initial file that was not written"
FAILED=1
else
echo "PASS: safe-start-on: built-in '$tag' -> initial_path inside the archive, file present"
fi
done
# The file has to be a real compiled rule-set, not a copy of the source
# JSON and not an empty file: sing-box IGNORES an initial file whose format
# does not match the declared one and blocks startup exactly as if there
# were none, which would make the whole feature a no-op that looks enabled.
# The magic is the check - it is what distinguishes a .srs from source JSON.
for tag in geoip-cn geosite-cn; do
f="$ss_initial/$tag.srs"
[ -s "$f" ] || continue
if [ "$(dd if="$f" bs=1 count=3 2>/dev/null)" = "SRS" ]; then
echo "PASS: safe-start-on: $tag.srs is a compiled rule-set (SRS magic)"
else
echo "FAIL: safe-start-on: $tag.srs is not a compiled rule-set - the first three"
echo " bytes are not the SRS magic, so sing-box would ignore the initial file"
FAILED=1
fi
done
# The fixture's nodes are all host names (a.example.com ...), so the
# domains file must name them. "The file exists" is NOT the assertion:
# join() with its arguments the wrong way round returns null, null + "\n"
# is the string "null\n", and the file is then non-empty while naming
# nothing. A released r41 shipped exactly that and dnsmasq went on to
# resolve a host called "null". So this greps for a name that is
# actually in the fixture.
if grep -qx 'a\.example\.com' "$na_dir/node-addr-domains.txt" 2>/dev/null; then
echo "PASS: node-addr: node-addr-domains.txt names the node ($(wc -l < "$na_dir/node-addr-domains.txt") host names)"
elif [ -s "$na_dir/node-addr-domains.txt" ]; then
echo "FAIL: node-addr: node-addr-domains.txt is non-empty but does not name a.example.com:"
sed 's/^/ /' "$na_dir/node-addr-domains.txt" | head -5
FAILED=1
else
echo "FAIL: node-addr: no node-addr-domains.txt; dnsmasq would get no nftset= snippet and"
echo " LAN connections to the node would be redirected into the proxy"
FAILED=1
fi
# The literal "null" is the specific failure above, and it is worth
# naming: it is the one value that satisfies a bare existence check.
if grep -qx 'null' "$na_dir/node-addr-domains.txt" 2>/dev/null \
|| grep -qx 'null' "$na_dir/node-addr-ips.txt" 2>/dev/null; then
echo "FAIL: node-addr: an exported file contains the literal 'null' - join() has its"
echo " arguments reversed (ucode takes the separator first)"
FAILED=1
fi
# A literal address cannot be back-filled by dnsmasq - there is no name for
# it to resolve - so firewall_post.ut renders it from this file instead.
if grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$|^[0-9a-fA-F:]+:[0-9a-fA-F:]*$' "$na_dir/node-addr-ips.txt" 2>/dev/null; then
echo "PASS: node-addr: node-addr-ips.txt holds literal addresses"
elif [ -s "$na_dir/node-addr-domains.txt" ]; then
# The fixture's node is a host name, so an empty ips file is the
# correct answer here rather than a silent failure.
echo "PASS: node-addr: no literal-address node in this fixture, ips file correctly empty"
else
echo "FAIL: node-addr: node-addr-ips.txt is missing and the domains file is empty too -"
echo " the generator did not export anything"
FAILED=1
fi
# An entry that is neither a host name nor an address is what the split is
# for; a bracketed IPv6 literal has to arrive unbracketed or the nft set
# element is a syntax error and fw4 rejects the whole ruleset.
if grep -q '\[' "$na_dir/node-addr-domains.txt" 2>/dev/null || grep -q '\[' "$na_dir/node-addr-ips.txt" 2>/dev/null; then
echo "FAIL: node-addr: an address kept its brackets; the nft set element would be invalid"
FAILED=1
else
echo "PASS: node-addr: no bracketed address leaked into either file"
fi
fi
# 8c/8d) opt-in on, user-defined remote rule-sets: one tag and two tags.
# One fixture covers both, plus a disabled one:
# tests/fixtures/generators/custom_safe_start.uci. A separate file rather
# than sed variations of custom.uci, because the opt-in belongs to the
# `config homeproxy-pro 'routing'` section - a different section than anything
# the existing variations anchor on - and because a two-expression sed that
# both inserts an option and appends a whole section is a construct this
# suite has never run. The one-expression form is proven here; the
# two-expression form is not, and a test whose own tooling is untested is
# how a case quietly stops testing anything.
run_case safe-start-user-remote "$ROOT/tests/fixtures/generators/custom_safe_start.uci" \
generate_client.uc sing-box-c.json
sr_json="$WORK/safe-start-user-remote/run/sing-box-c.json"
sr_initial="$WORK/safe-start-user-remote/ruleset/initial"
if [ ! -f "$sr_json" ]; then
echo "FAIL: safe-start-user-remote: no config was generated"
FAILED=1
else
# Single tag: a concrete path, and the file it names has to be there. A
# pointer at a file that does not exist is not a degraded feature, it is no
# feature - sing-box ignores it and blocks startup exactly as before.
# Same reason as 8b: assert the exact string, not a range that can run on
# into the next entry.
sr_want="\"initial_path\": \"$sr_initial/cfg-rs_remote-rule.srs\""
if ! grep -qF "$sr_want" "$sr_json"; then
echo "FAIL: safe-start-user-remote: the single-tag rule-set does not carry"
echo " $sr_want"
grep -n 'initial_path' "$sr_json" | sed 's/^/ /'
FAILED=1
fi
if [ -s "$sr_initial/cfg-rs_remote-rule.srs" ]; then
echo "PASS: safe-start-user-remote: single-tag -> concrete initial_path, file present"
else
echo "FAIL: safe-start-user-remote: no fallback file for cfg-rs_remote-rule.srs"
FAILED=1
fi
# Multi tag: {tag} in the path, and one file per tag. The entry is matched
# on the multi-tag shape (a tag ARRAY) rather than on the section name, so
# the assertion cannot be satisfied by the single-tag entry above.
mt_want="\"initial_path\": \"$sr_initial/{tag}.srs\""
if ! grep -qF "$mt_want" "$sr_json"; then
echo "FAIL: safe-start-user-remote: the multi-tag rule-set does not carry"
echo " $mt_want"
echo " sing-box substitutes {tag} and opens every resulting file, so a"
echo " literal path would make it fetch and block - the failure this removes"
grep -n 'initial_path' "$sr_json" | sed 's/^/ /'
FAILED=1
fi
mt_missing=""
for tag in cfg-rs_multi-rule cfg-alt-rule; do
[ -s "$sr_initial/$tag.srs" ] || mt_missing="$mt_missing $tag"
done
if [ -n "$mt_missing" ]; then
echo "FAIL: safe-start-user-remote: no fallback file for:$mt_missing"
echo " sing-box opens every tag's file, so a missing one blocks startup"
FAILED=1
else
echo "PASS: safe-start-user-remote: multi-tag -> {tag} placeholder, one file per tag"
fi
# Disabled: build_user_rulesets() skips it, so it must be absent from the
# config AND must not have caused a file to be written for it.
if grep -q 'cfg-rs_off-rule' "$sr_json"; then
echo "FAIL: safe-start-user-remote: a disabled rule-set reached the generated config"
FAILED=1
elif [ -e "$sr_initial/cfg-rs_off-rule.srs" ]; then
echo "FAIL: safe-start-user-remote: a fallback file was written for a disabled rule-set"
FAILED=1
else
echo "PASS: safe-start-user-remote: a disabled rule-set gets neither path nor file"
fi
fi
# 6) P3 #8 (extra_tags die() on missing {tag}): deferred to
# a dedicated 'testbed-dialect' sprint. The multi-line sed to
# attach an extra_tags list onto the existing rs_local ruleset
@@ -224,9 +224,9 @@ check('certificate_write tolerates an empty request', threw == null, threw);
* executed. Four of these were referenced by no test at all. */
const all_methods = [
'acllist_read', 'acllist_write', 'certificate_write', 'connection_check',
'log_clean', 'node_parse', 'resources_get_version', 'resources_update',
'singbox_generator', 'singbox_get_features', 'update_subscriptions',
'update_subscriptions_status'
'debug_report', 'log_clean', 'node_parse', 'resources_get_version',
'resources_update', 'singbox_generator', 'singbox_get_features',
'update_subscriptions', 'update_subscriptions_status'
];
for (let m in all_methods) {
@@ -249,6 +249,78 @@ check('connection_check rejects an unknown site',
check('log_clean rejects an unknown log type',
rpc.log_clean.call({ args: { type: '../etc/passwd' } }).status !== 0);
/* --- debug_report -------------------------------------------------------
*
* This one is worth more than a shape check, because the property that
* makes it usable is the opposite of "works on my router": it has to
* produce a complete, readable report on a machine that has none of the
* runtime - no nft, no sing-box, no apk, no /etc/config/homeproxy-pro. That is
* exactly this test host, so a regression that turns a failed probe into an
* exception, or drops a section when its command is missing, fails here
* rather than on a user's router where the report is the thing they are
* relying on. */
import { isSecretKey } from '@@SCRIPTS@@/runtime/debug_report.uc';
const dr = rpc.debug_report.call({ args: {} });
check('debug_report succeeds where no runtime is installed', dr.result === true,
dr.error);
check('debug_report names the file it wrote', type(dr.path) === 'string' && length(dr.path) > 0);
check('debug_report reports a non-zero size', dr.size > 0);
const dr_text = readfile(sprintf('%s/run/debug.log', WORK));
check('the report was written where the ACL grants read', dr_text != null && length(dr_text) > 0);
if (dr_text != null && length(dr_text) > 0) {
check('the report carries a title', index(dr_text, '# HomeProxy ') != -1);
check('the report carries a privacy notice', index(dr_text, '脱敏') != -1);
/* Every section header, checked as a literal substring rather than a
* regex: the headers carry CJK and full-width brackets, and a probe that
* throws would take the whole report down rather than leave one section
* missing - so "the section is still there" is the property worth pinning. */
for (let h in ['## 1. ', '## 2. ', '## 3. ', '## 4. ', '## 5. ',
'## 6. ', '## 7. ', '## 8. ', '## 9. ', '## 10. '])
check(sprintf('the report still has section %s', h), index(dr_text, h) != -1);
/* A missing command must be said out loud. A report that silently omits
* the firewall dump reads exactly like "the firewall is empty", which is
* the opposite of what happened. */
check('an unavailable probe is labelled, not omitted',
index(dr_text, 'unavailable') != -1 || index(dr_text, '(no output)') != -1);
}
/* The redaction rule has no I/O to observe, so it is asserted directly: a
* key that quietly stops being recognised is a credential in an issue
* thread, and nothing else in this suite would notice. */
for (let k in ['password', 'uuid', 'secret', 'token', 'private_key', 'url',
'urls', 'subscription_urls', 'address', 'tls_sni', 'username',
'hysteria_obfs_password', 'tls_reality_public_key',
'tls_reality_short_id', 'ws_path', 'hysteria_obfs_method',
'auth'])
check(sprintf('the key %s is redacted', k), isSecretKey(k));
/* ...and the other direction matters just as much: over-broad matching
* would blank out the very switches the report exists to show. */
for (let k in ['proxy_mode', 'routing_mode', 'mixed_port', 'redirect_port',
'self_mark', 'tproxy_mark', 'log_level', 'ipv6_support',
'enabled', 'grouphash'])
check(sprintf('the key %s is left readable', k), !isSecretKey(k));
/* The urltest lists are the reason `url` is anchored rather than a bare
* substring: `main_urltest_nodes` contains the letters u-r-l, so an
* unanchored pattern blanked the pool out of the report - and "is this node
* still in the test pool" is exactly what the report is read for. These
* three were found by a canary run, not by inspection. */
for (let k in ['main_urltest_nodes', 'main_urltest_tolerance',
'main_udp_urltest_nodes'])
check(sprintf('the key %s is left readable (urltest, not url)', k), !isSecretKey(k));
/* grpc_servicename names the gRPC service, it is not a credential: masking
* it would hide which transport a node uses, which is worth reporting.
* Asserted on the readable side because the obvious guess is that anything
* shaped like a service identifier should be hidden. */
check('the key grpc_servicename is left readable', !isSecretKey('grpc_servicename'));
printf('rpc methods: %d checks, %d failures\n', checks, failures);
exit(failures ? 1 : 0);
DRIVER
@@ -259,7 +331,8 @@ DRIVER
# `methods` and the driver drives it in the same scope. Nothing else about the
# file changes.
sed '$d' "$WORK/rpc.uc" > "$WORK/run.uc"
sed -e "s#@@WORK@@#$WORK#" -e "s#@@STAGE@@#$WORK/tmp#" "$WORK/driver_body.uc" >> "$WORK/run.uc"
sed -e "s#@@WORK@@#$WORK#" -e "s#@@STAGE@@#$WORK/tmp#" -e "s#@@SCRIPTS@@#$WORK/scripts#g" \
"$WORK/driver_body.uc" >> "$WORK/run.uc"
if ucode -L "$WORK/scripts" -L "$WORK" "$WORK/run.uc"; then
echo "PASS: rpc method behaviour"