mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-03 23:03:56 +08:00
💐 Sync 2026-10-06 12:33:44
This commit is contained in:
1 parent
8be03ce939
commit
7fe69dbb3a
246 files changed
+50330
-3414
No files matched your search
@@ -601,6 +601,108 @@ hp_capture_candidate() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# hp_reload_is_noop <run-dir> <good-dir> <client-enabled> <server-enabled>
|
||||
# 0 when this reload would change nothing that is currently running, so the
|
||||
# caller can skip the stop;start entirely.
|
||||
#
|
||||
# Why this exists: reload_service used to stop and start unconditionally, and
|
||||
# hp_same_file was only ever used on the rollback path. Measured on the device,
|
||||
# one reload costs about eight seconds (09:17:40 -> 09:17:48 in the log) and
|
||||
# interrupts every connection, even when the generated configuration is
|
||||
# byte-identical to the one already running. That is the common case for
|
||||
# "Save & Apply" with nothing edited, for a node switch that lands on the same
|
||||
# node, and for the nightly resource refresh.
|
||||
#
|
||||
# THE COMPARISON IS AGAINST known-good, NOT AGAINST THE CANDIDATE, and getting
|
||||
# that wrong silently disables the whole mechanism. Measured on the device:
|
||||
# the generator writes straight to $RUN_DIR/sing-box-c.json and
|
||||
# hp_capture_candidate() copies that same file afterwards, so after a preflight
|
||||
# the candidate and the live file are the SAME BYTES BY CONSTRUCTION - comparing
|
||||
# them reports "unchanged" for every reload, including one that switched the main
|
||||
# node. The first version of this helper did exactly that, and the only reason
|
||||
# it was caught is an on-device test that switched nodes and watched the PID.
|
||||
#
|
||||
# known-good is the right counterpart for the same reason the rollback path uses
|
||||
# it: hp_promote_known_good writes it ONLY after the health gate has passed, so
|
||||
# it is by definition "the configuration that is currently running and working".
|
||||
# live vs known-good answers the question this helper is actually asking - "would
|
||||
# the running configuration change?" - and a missing known-good simply falls
|
||||
# through to a real reload.
|
||||
#
|
||||
# Every condition is a *precondition for the skip being safe*, and the default
|
||||
# on any doubt is to reload:
|
||||
#
|
||||
# [1] the service is actually running. A live file left over from a crashed
|
||||
# instance is not a running service, and skipping the restart would
|
||||
# report success over a dead proxy. Asked of ubus, the same source the
|
||||
# status page reads.
|
||||
# [2] the side has both a live file and a known-good copy to compare against.
|
||||
# A side the user just enabled has neither, and that is a change, not a
|
||||
# no-op.
|
||||
# [3] the intercept layer is installed. A released layer (the marker
|
||||
# hp_release_stale_intercept writes) means the firewall is NOT proxying
|
||||
# right now even though sing-box is up. Skipping the restart would leave
|
||||
# it that way and log "nothing to restart" - the exact silent state
|
||||
# hp_rearm_intercept exists to undo. Reloading is what re-arms it.
|
||||
hp_reload_is_noop() {
|
||||
local run_dir="$1"
|
||||
local good_dir="$2"
|
||||
local client_enabled="$3"
|
||||
local server_enabled="$4"
|
||||
local live good
|
||||
|
||||
# [3] first, because it is the cheapest and the most dangerous to skip.
|
||||
[ ! -f "$run_dir/intercept-released" ] || return 1
|
||||
|
||||
# [1] the instances. `command -v ubus` because this module is also sourced
|
||||
# by the off-target test driver, where ubus does not exist and the honest
|
||||
# answer there is "cannot prove it is a no-op".
|
||||
#
|
||||
# The check is on `"running": true`, NOT on the instance merely appearing
|
||||
# in the list: procd keeps a stopped instance in `service list` with
|
||||
# running=false, so matching the key alone reports a dead proxy as running
|
||||
# and the skip then claims success over it. That was not theoretical - the
|
||||
# first version of this grep did exactly that and its own test caught it.
|
||||
command -v ubus > "/dev/null" 2>&1 || return 1
|
||||
|
||||
local svc_list
|
||||
svc_list="$(ubus call service list "{\"name\":\"$CONF\"}" 2>/dev/null)"
|
||||
|
||||
if [ "$client_enabled" = "1" ]; then
|
||||
printf '%s' "$svc_list" \
|
||||
| grep -A 3 '"sing-box-c"' \
|
||||
| grep -q '"running"[[:space:]]*:[[:space:]]*true' || return 1
|
||||
fi
|
||||
|
||||
if [ "$server_enabled" = "1" ]; then
|
||||
printf '%s' "$svc_list" \
|
||||
| grep -A 3 '"sing-box-s"' \
|
||||
| grep -q '"running"[[:space:]]*:[[:space:]]*true' || return 1
|
||||
fi
|
||||
|
||||
# [2] and the byte comparison, per side.
|
||||
# [2] and the byte comparison, per side, against what is known to be running
|
||||
# rather than against the candidate (see the note at the top of this
|
||||
# function - the two are the same bytes by construction).
|
||||
if [ "$client_enabled" = "1" ]; then
|
||||
live="$run_dir/sing-box-c.json"
|
||||
good="$good_dir/sing-box-c.json"
|
||||
|
||||
[ -s "$live" ] && [ -s "$good" ] || return 1
|
||||
cmp -s "$live" "$good" 2>"/dev/null" || return 1
|
||||
fi
|
||||
|
||||
if [ "$server_enabled" = "1" ]; then
|
||||
live="$run_dir/sing-box-s.json"
|
||||
good="$good_dir/sing-box-s.json"
|
||||
|
||||
[ -s "$live" ] && [ -s "$good" ] || return 1
|
||||
cmp -s "$live" "$good" 2>"/dev/null" || return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# hp_start_generated_config <side> <hp-dir> <run-dir> <good-dir>
|
||||
# The start-path transaction for one side ("c" or "s"):
|
||||
#
|
||||
|
||||
@@ -248,15 +248,41 @@ check_list_update() {
|
||||
# china_ip4.json exists: one list with two readers, and nothing for
|
||||
# a cold start to download before the inbounds bind.
|
||||
#
|
||||
# The sed above rewrote the file in place, so this runs after it
|
||||
# and sees the cleaned list. `full:` prefixes and anything with a
|
||||
# colon are gone by now; what is left is what the helper accepts.
|
||||
if ucode -S "$SCRIPT_DIR/runtime/domain_ruleset.uc" \
|
||||
"$RESOURCES_DIR/$listtype.txt" "$RESOURCES_DIR/china-domain.json" >>"$LOG_PATH" 2>&1; then
|
||||
log "[CHINA_LIST] DNS-side rule-set regenerated."
|
||||
chown sing-box:sing-box "$RESOURCES_DIR/china-domain.json" 2>"/dev/null"
|
||||
# Normalise FIRST, generate second - and both inside this branch.
|
||||
# The order is load-bearing, and it was previously wrong: the sed
|
||||
# lived in the `case "china_list"` arm, i.e. after this function had
|
||||
# already returned, so the generator was handed the RAW download.
|
||||
# That is not a theoretical difference. Upstream ships `full:`
|
||||
# prefixes and the colon-carrying `regexp:` / `keyword:` forms
|
||||
# still in the file - 562 of 111,361 lines in the release measured
|
||||
# here - and domain_ruleset.uc rejects every entry containing a
|
||||
# colon, so 554 real domains were dropped from the DNS split and
|
||||
# the only trace was a "skipped N malformed entries" line naming a
|
||||
# list that was perfectly well formed.
|
||||
#
|
||||
# Not `sed -i`: that form is a busybox/GNU extension, and the same
|
||||
# script is exercised off-device where a non-busybox sed fails it
|
||||
# with "invalid command code". Edit through a temp file, the way
|
||||
# the crontab helper in runtime/service.sh does.
|
||||
#
|
||||
# A normalisation that fails does not undo the install - the list
|
||||
# on disk is the one the upstream commit vouched for, and the
|
||||
# firewall renders its nft set from that very file. The generator
|
||||
# is skipped instead, so the DNS side keeps the previous rule-set
|
||||
# rather than being rebuilt from a list the sed only half-processed.
|
||||
if sed -e "s/full://g" -e "/:/d" "$RESOURCES_DIR/$listtype.txt" \
|
||||
> "$RESOURCES_DIR/$listtype.txt.hp-new" \
|
||||
&& mv -f "$RESOURCES_DIR/$listtype.txt.hp-new" "$RESOURCES_DIR/$listtype.txt"; then
|
||||
if ucode -S "$SCRIPT_DIR/runtime/domain_ruleset.uc" \
|
||||
"$RESOURCES_DIR/$listtype.txt" "$RESOURCES_DIR/china-domain.json" >>"$LOG_PATH" 2>&1; then
|
||||
log "[CHINA_LIST] DNS-side rule-set regenerated."
|
||||
chown sing-box:sing-box "$RESOURCES_DIR/china-domain.json" 2>"/dev/null"
|
||||
else
|
||||
log "[CHINA_LIST] Warning: could not regenerate china-domain.json (list has no usable domain entry?); the DNS side keeps the previous list."
|
||||
fi
|
||||
else
|
||||
log "[CHINA_LIST] Warning: could not regenerate china-domain.json (list has no usable domain entry?); the DNS side keeps the previous list."
|
||||
rm -f "$RESOURCES_DIR/$listtype.txt.hp-new"
|
||||
log "[CHINA_LIST] Warning: could not normalise the downloaded list; it is installed as downloaded and the DNS side keeps the previous rule-set."
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
@@ -280,10 +306,13 @@ case "$1" in
|
||||
check_list_update "$1" "Loyalsoldier/v2ray-rules-dat" "release" "gfw.txt"
|
||||
;;
|
||||
"china_list")
|
||||
# Not `sed -i`: the bare -i form is a busybox/GNU extension, and the same
|
||||
# script is exercised off-device where a non-busybox sed fails it with
|
||||
# "invalid command code". Edit through a temp file, the way the crontab
|
||||
# helper in runtime/service.sh does.
|
||||
# The `full:` prefixes and the colon-carrying regexp:/keyword: forms are
|
||||
# stripped inside check_list_update, BEFORE the DNS-side rule-set is
|
||||
# generated from the list - domain_ruleset.uc rejects any entry containing
|
||||
# a colon, so the order decides whether 554 real domains are in the split
|
||||
# or silently skipped. See the case branch there; this arm is only the
|
||||
# list's own upstream coordinates.
|
||||
#
|
||||
# The status has to survive the post-processing. As one &&...|| chain it
|
||||
# did not: when check_list_update returned 3 ("already current") or 1
|
||||
# (fetch failed) the chain short-circuited into `rm -f`, whose status 0
|
||||
@@ -293,18 +322,7 @@ case "$1" in
|
||||
# Measured on the device: china_list exited 0 for both the failure and the
|
||||
# up-to-date case, while china_ip4 exited 1 / 3 correctly.
|
||||
check_list_update "$1" "Loyalsoldier/v2ray-rules-dat" "release" "direct-list.txt"
|
||||
rc=$?
|
||||
[ "$rc" = "0" ] || exit "$rc"
|
||||
|
||||
# 0 means a new list was just installed; strip the `full:` prefixes and the
|
||||
# colon-carrying entries before anything else reads it.
|
||||
if sed -e "s/full://g" -e "/:/d" "$RESOURCES_DIR/china_list.txt" > "$RESOURCES_DIR/china_list.txt.hp-new" \
|
||||
&& mv -f "$RESOURCES_DIR/china_list.txt.hp-new" "$RESOURCES_DIR/china_list.txt"; then
|
||||
exit 0
|
||||
fi
|
||||
rm -f "$RESOURCES_DIR/china_list.txt.hp-new"
|
||||
log "[CHINA_LIST] Failed to normalise the downloaded list; leaving it as downloaded."
|
||||
exit 1
|
||||
exit $?
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' "Usage: $0 <china_ip4 / china_ip6 / gfw_list / china_list>"
|
||||
|
||||
@@ -77,10 +77,32 @@ import { Loader } from './config/loader.uc';
|
||||
* A2: the run performs exactly one uci.commit(). The Repository modules stage
|
||||
* mutations only.
|
||||
*
|
||||
* UCICONFIG_DIR (homeproxy-pro.uc) is the config *directory* Loader.load() takes,
|
||||
* and the sandboxed test points it at its own config instead of /etc/config
|
||||
* (tests/ucode/test_subscription_updater_runs.sh rewrites that line in
|
||||
* homeproxy-pro.uc and fails loudly if the rewrite did not apply). */
|
||||
* UCICONFIG_DIR (homeproxy-pro.uc) is the config *directory* this script reads
|
||||
* and writes, and the sandboxed test points it at its own config instead of
|
||||
* /etc/config (tests/ucode/test_subscription_updater_runs.sh rewrites that
|
||||
* line in homeproxy-pro.uc and fails loudly if the rewrite did not apply).
|
||||
*
|
||||
* That rewrite is only half the sandbox, and the half that was missing is the
|
||||
* dangerous one. `cursor()` with no argument is a cursor on the REAL
|
||||
* /etc/config, not on UCICONFIG_DIR - the constant and the cursor were never
|
||||
* connected. Measured on the device:
|
||||
*
|
||||
* cursor() -> reads infra, config, control, subscription, and the
|
||||
* four live node sections
|
||||
* cursor('/tmp/t8cfg') -> reads nothing
|
||||
*
|
||||
* So the updater was reading the live configuration whatever the test staged,
|
||||
* and `uci.commit(uciconfig)` - the one commit this script performs, at the
|
||||
* end of a successful run - would have written the LIVE /etc/config/homeproxy-pro.
|
||||
* The committed test only ever drove a run whose fetch failed before that
|
||||
* commit, which is why nothing had touched the device yet. Any future case
|
||||
* that lets a fetch succeed would have committed the test's sandbox nodes over
|
||||
* the user's real configuration.
|
||||
*
|
||||
* Hence the same `dir` injection Loader.load() already uses (loader.uc:385):
|
||||
* the cursor is built on UCICONFIG_DIR, so the constant the test already
|
||||
* rewrites is finally the thing that decides where both the read and the write
|
||||
* land. Production is unchanged - UCICONFIG_DIR is /etc/config there. */
|
||||
const CONFIG_FILE = UCICONFIG_DIR + '/homeproxy-pro';
|
||||
const uciconfig = 'homeproxy-pro';
|
||||
|
||||
@@ -97,7 +119,10 @@ let uci, loaded, sub, routing_mode,
|
||||
/* Read everything the run needs, under the lock. Read-only: nothing here
|
||||
* mutates, so a failure cannot leave a partial state behind. */
|
||||
function load_locked_state() {
|
||||
uci = cursor();
|
||||
/* On UCICONFIG_DIR, not on the default. See the note on CONFIG_FILE:
|
||||
* a bare cursor() here reads - and, at the end of a successful run,
|
||||
* commits - the live /etc/config, no matter what the test staged. */
|
||||
uci = cursor(UCICONFIG_DIR);
|
||||
uci.load(uciconfig);
|
||||
|
||||
/* The orchestrator used to hold its own
|
||||
|
||||
@@ -605,6 +605,27 @@ reload_service() {
|
||||
# same way in the rollback path below.
|
||||
export HP_CANDIDATE_CLIENT HP_CANDIDATE_SERVER
|
||||
|
||||
# Nothing to do? The candidates are byte-identical to what is already
|
||||
# running and the running instance is healthy, so the stop;start below
|
||||
# would interrupt every connection to install a file that is already
|
||||
# installed. Checked AFTER the preflight, never before: the point is to
|
||||
# skip a restart, not to skip the validation - a configuration that fails
|
||||
# `sing-box check` has to be reported as such whether or not it would have
|
||||
# been a no-op, and the candidate has to exist for anything to compare.
|
||||
#
|
||||
# Every "cannot prove it is a no-op" case falls through to the normal
|
||||
# reload, so the worst outcome of this helper is the behaviour that
|
||||
# existed before it.
|
||||
#
|
||||
# `HP_CANDIDATE_CLIENT` is the same flag the preflight set, so the two
|
||||
# sides cannot disagree about which configuration is being activated.
|
||||
if [ "$HP_CANDIDATE_CLIENT" = "1" ] || [ "$HP_CANDIDATE_SERVER" = "1" ]; then
|
||||
if hp_reload_is_noop "$RUN_DIR" "$GOOD_DIR" "$HP_CANDIDATE_CLIENT" "$HP_CANDIDATE_SERVER"; then
|
||||
log "Configuration is unchanged and the service is healthy; nothing to restart."
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Both configurations passed `sing-box check`; now it is safe to restart.
|
||||
# start_service owns the rest of the transaction: it proves the instances
|
||||
# came up and only then records the new known-good, so the previous one
|
||||
|
||||
Reference in new issue
Block a user