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

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

No files matched your search

@@ -300,116 +300,6 @@ export function ruleSetFormatFromPath(path) {
return null;
};
/* --- the "do not block startup on the first download" fallback ---------- *
*
* A remote rule-set with no `initial_path` is fetched during
* initialization, BEFORE the inbounds bind. On a cold cache that means a
* fresh install cannot start until raw.githubusercontent.com answers - and if
* it does not, the whole instance dies with a FATAL and the health gate turns
* that into a rollback or a released intercept layer. The node has to be up
* too, because these downloads go through http_clients, so a working
* configuration can be kept off the air by an unreachable CDN.
*
* `initial_path` is the way out: sing-box reads the file first, starts
* immediately, and refreshes from the URL in the background. A download that
* keeps failing then becomes an ERROR line in the log instead of a failed
* start - and the health gate does not look at rule-sets at all, so a
* degraded rule-set is promoted like any other healthy start.
*
* What the file holds is an EMPTY rule-set, and that is the whole trade: a
* rule-set with no rules matches nothing, so its traffic falls through to
* `final`. That changes routing, so this is opt-in (config.ruleset_safe_start,
* default off) and never the default.
*
* The format has to match the declared one. A binary rule-set fed a source
* JSON is not read at all - sing-box ignores the mismatch and blocks exactly
* as it does with no initial_path - so the decision of whether a fallback is
* even possible is made once, here, from the same inputs on both sides. */
/* Where the generated fallbacks live. Inside the rule-set archive, so
* validateRuleSetPath() admits it and the jail's read-only mount of HP_DIR
* already exposes it to the sing-box user. */
export const RULESET_INITIAL_DIR = HP_DIR + '/ruleset/initial';
/* The two files the package ships for this. empty.source.json is what
* `sing-box rule-set compile` consumes (24 bytes, readable, and the form
* every version agrees on); empty.srs is the compiled result of THAT source
* on 1.14.2, kept as the last resort for a router where the compile cannot
* run.
*
* The .srs is committed rather than built at package time on purpose: it is a
* 14-byte binary, and having it in the tree means `hexdump -C` on a router
* can confirm byte for byte that the shipped constant is the documented one.
* It is deliberately NOT a conffile - it is ours, not the user's, and an
* upgrade may replace it freely.
*
* Compatibility note, since a hard-coded binary artifact invites the question:
* sing-box only rejects an SRS whose version is NEWER than its own, so a
* version-2 file stays readable. The compile path exists anyway, because a
* file produced by the kernel that is running it cannot drift from it. */
export const RULESET_EMPTY_SOURCE = RULESET_INITIAL_DIR + '/empty.source.json';
export const RULESET_EMPTY_BINARY = RULESET_INITIAL_DIR + '/empty.srs';
/* A tag becomes a file name, so it has to be one. UCI section names are
* already restricted, but a rule_set `tag` is a sing-box identifier that UCI
* does not fully own, and this value is about to be concatenated into a path
* the generator writes as root. Anything outside this set is refused, and
* the caller skips the fallback rather than sanitising it into a name that
* could collide with another rule-set's. */
export function isSafeRuleSetTag(tag) {
if (!tag || type(tag) !== 'string')
return false;
return match(tag, /^[A-Za-z0-9][A-Za-z0-9._-]*$/) != null;
};
/* The extension a format is stored under. Null for anything that is not one
* of the two formats, which is the caller's signal that no fallback is
* possible rather than something to guess at. */
export function ruleSetFormatExtension(format) {
if (format === 'binary')
return 'srs';
if (format === 'source')
return 'json';
return null;
};
/* ruleSetInitialFallback(tags, declared, url) -> the initial_path to emit, or
* null when no fallback is possible.
*
* One function, called from both sides on purpose. The CLI asks it to
* decide which files to create and the generator asks it where to point
* `initial_path`; if they each worked it out separately they could disagree
* about the format, and a mismatch is the failure this whole mechanism exists
* to avoid - sing-box ignores a wrong-format initial file and blocks startup
* exactly as if there were none.
*
* `declared` is the rule-set's own `format`, which for a rule-set with no
* initial file is also the format the generator emits (nothing to probe, so
* nothing is corrected). With no declared format the URL's extension is the
* only remaining evidence; with neither, the answer is null and the rule-set
* keeps today's behaviour rather than being handed a file of the wrong shape.
*
* More than one tag gets a `{tag}` placeholder, because that is what sing-box
* substitutes and it requires every tag's file to exist - P3 on 1.14.2: a
* literal "{tag}.srs" on disk makes it try to download and block. */
export function ruleSetInitialFallback(tags, declared, url) {
const format = declared || ruleSetFormatFromPath(url);
const ext = ruleSetFormatExtension(format);
if (!ext)
return null;
for (let tag in (tags || []))
if (!isSafeRuleSetTag(tag))
return null;
const stem = (length(tags || []) > 1) ? '{tag}' : tags[0];
return RULESET_INITIAL_DIR + '/' + stem + '.' + ext;
};
/* probeRuleSetFile(path) -> 'binary' | 'source' | null.
*