mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-02 05:13:56 +08:00
🎈 Sync 2026-10-04 14:38:18
This commit is contained in:
1 parent
0e6611f767
commit
aba12667c5
88 files changed
+4600
-1458
No files matched your search
@@ -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). |
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user