Fix subscription proxy groups in generator
This commit is contained in:
@@ -128,7 +128,7 @@ function uci_cursor() {
|
||||
}
|
||||
|
||||
function runtime_generate_unsupported(reason) {
|
||||
warn("unsupported: ", reason, "\n");
|
||||
warn(reason, "\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
@@ -164,6 +164,17 @@ function internal_flag(value) {
|
||||
return value === true || value == 1 || value == "1" || value == "true" || value == "yes";
|
||||
}
|
||||
|
||||
function subscription_group_outbound(outbound) {
|
||||
if (type(outbound) != "object")
|
||||
return false;
|
||||
let t = as_string(outbound.type);
|
||||
return (t == "selector" || t == "urltest") && internal_flag(outbound.__podkop_allow_group);
|
||||
}
|
||||
|
||||
function subscription_hidden_outbound(outbound) {
|
||||
return type(outbound) == "object" && internal_flag(outbound.__podkop_hidden);
|
||||
}
|
||||
|
||||
function tag(base, postfix) {
|
||||
return runtime_constants.tag(base, postfix);
|
||||
}
|
||||
@@ -365,6 +376,8 @@ function supported_subscription_outbound(outbound) {
|
||||
if (type(outbound) != "object")
|
||||
return false;
|
||||
let t = as_string(outbound.type);
|
||||
if (subscription_group_outbound(outbound))
|
||||
return true;
|
||||
if (t == "direct" || t == "selector" || t == "urltest" || t == "dns" || t == "block")
|
||||
return false;
|
||||
return t == "vless" || t == "vmess" || t == "trojan" || t == "shadowsocks" ||
|
||||
@@ -382,6 +395,38 @@ function copy_subscription_outbound(outbound, new_tag) {
|
||||
return copy;
|
||||
}
|
||||
|
||||
function rewrite_subscription_outbound_references(outbounds, tag_map) {
|
||||
for (let outbound in outbounds) {
|
||||
if (type(outbound) != "object")
|
||||
continue;
|
||||
|
||||
let detour = as_string(outbound.detour || "");
|
||||
if (detour != "" && tag_map[detour])
|
||||
outbound.detour = tag_map[detour];
|
||||
|
||||
if (type(outbound.outbounds) == "array") {
|
||||
let rewritten = [];
|
||||
for (let tag_name in outbound.outbounds) {
|
||||
tag_name = as_string(tag_name);
|
||||
if (tag_map[tag_name])
|
||||
push(rewritten, tag_map[tag_name]);
|
||||
}
|
||||
outbound.outbounds = rewritten;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function subscription_skip_summary(skipped) {
|
||||
let parts = [];
|
||||
for (let t in sort(keys(skipped)))
|
||||
push(parts, skipped[t] + "x " + t);
|
||||
return join("; ", parts);
|
||||
}
|
||||
|
||||
function reportable_skipped_subscription_type(t) {
|
||||
return t != "direct" && t != "selector" && t != "urltest" && t != "dns" && t != "block";
|
||||
}
|
||||
|
||||
function unique_tag(base, taken) {
|
||||
base = as_string(base);
|
||||
if (base == "")
|
||||
@@ -396,7 +441,7 @@ function unique_tag(base, taken) {
|
||||
return base + "-overflow";
|
||||
}
|
||||
|
||||
function add_subscription_source_with_state(config, section_name, source_index, source_entry, taken, selector_tags, state) {
|
||||
function add_subscription_source_with_state(config, section_name, source_index, source_entry, taken, selector_tags, detour_tags, state) {
|
||||
let source_section = runtime_subscription.source_id(section_name, source_index);
|
||||
if (!runtime_subscription.source_cache_is_current(source_section, source_entry))
|
||||
return 0;
|
||||
@@ -406,21 +451,60 @@ function add_subscription_source_with_state(config, section_name, source_index,
|
||||
return 0;
|
||||
|
||||
runtime_subscription.merge_source_metadata(state, section_name, source_section, source_index, source_entry);
|
||||
let prepared = [];
|
||||
let source_indices = [];
|
||||
let display_names = [];
|
||||
let group_flags = [];
|
||||
let hidden_flags = [];
|
||||
let tag_map = {};
|
||||
let skipped = {};
|
||||
for (let i = 0; i < length(outbounds); i++) {
|
||||
let outbound = outbounds[i];
|
||||
if (!supported_subscription_outbound(outbound))
|
||||
runtime_generate_unsupported("subscription outbound type is not supported by sing-box config generation");
|
||||
if (!supported_subscription_outbound(outbound)) {
|
||||
let t = type(outbound) == "object" ? as_string(outbound.type || "missing-type") : "non-object";
|
||||
if (reportable_skipped_subscription_type(t))
|
||||
skipped[t] = (skipped[t] || 0) + 1;
|
||||
continue;
|
||||
}
|
||||
let display_name = as_string(outbound.remark || outbound.tag || ("server-" + (i + 1)));
|
||||
let base = as_string(outbound.tag || outbound.remark || ("server-" + (i + 1)));
|
||||
let new_tag = unique_tag(base, taken);
|
||||
taken[new_tag] = true;
|
||||
push(config.outbounds, copy_subscription_outbound(outbound, new_tag));
|
||||
if (!internal_flag(outbound.__podkop_hidden)) {
|
||||
push(selector_tags, new_tag);
|
||||
runtime_subscription.remember_visible_outbound(state, new_tag, source_section, source_index, i + 1, display_name, outbound);
|
||||
tag_map[base] = new_tag;
|
||||
if (as_string(outbound.tag || "") != "")
|
||||
tag_map[as_string(outbound.tag)] = new_tag;
|
||||
if (as_string(outbound.remark || "") != "")
|
||||
tag_map[as_string(outbound.remark)] = new_tag;
|
||||
push(prepared, copy_subscription_outbound(outbound, new_tag));
|
||||
push(source_indices, i + 1);
|
||||
push(display_names, display_name);
|
||||
push(group_flags, subscription_group_outbound(outbound));
|
||||
push(hidden_flags, subscription_hidden_outbound(outbound));
|
||||
}
|
||||
|
||||
if (length(keys(skipped)) > 0)
|
||||
warn("skipped unsupported subscription outbounds for rule '", section_name, "': ", subscription_skip_summary(skipped), "\n");
|
||||
|
||||
rewrite_subscription_outbound_references(prepared, tag_map);
|
||||
let added = 0;
|
||||
for (let i = 0; i < length(prepared); i++) {
|
||||
let outbound = prepared[i];
|
||||
let is_group = group_flags[i] === true;
|
||||
if (is_group && length(array_or_empty(outbound.outbounds)) == 0) {
|
||||
warn("skipped empty subscription group for rule '", section_name, "': ", as_string(display_names[i] || outbound.tag || "unknown"), "\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
push(config.outbounds, outbound);
|
||||
added++;
|
||||
if (!is_group)
|
||||
push(detour_tags, outbound.tag);
|
||||
if (hidden_flags[i] !== true) {
|
||||
push(selector_tags, outbound.tag);
|
||||
runtime_subscription.remember_visible_outbound(state, outbound.tag, source_section, source_index, source_indices[i], display_names[i], outbound);
|
||||
}
|
||||
}
|
||||
return length(outbounds);
|
||||
return added;
|
||||
}
|
||||
|
||||
function duration_to_seconds(value) {
|
||||
@@ -1114,19 +1198,22 @@ function add_proxy_outbound(config, section, taken) {
|
||||
let subscription_urls = list_option(section, "subscription_urls");
|
||||
|
||||
let selector_tags = [];
|
||||
let detour_tags = [];
|
||||
let state = runtime_subscription.new_section_state(section_name);
|
||||
let udp_over_tcp = bool_option(section, "enable_udp_over_tcp", false);
|
||||
let manual_links = list_option(section, "selector_proxy_links");
|
||||
for (let i = 0; i < length(manual_links); i++)
|
||||
for (let i = 0; i < length(manual_links); i++) {
|
||||
add_manual_proxy_link(config, state, section_name, i + 1, manual_links[i], udp_over_tcp, taken, selector_tags);
|
||||
push(detour_tags, selector_tags[length(selector_tags) - 1]);
|
||||
}
|
||||
|
||||
for (let i = 0; i < length(subscription_urls); i++)
|
||||
add_subscription_source_with_state(config, section_name, i + 1, subscription_urls[i], taken, selector_tags, state);
|
||||
add_subscription_source_with_state(config, section_name, i + 1, subscription_urls[i], taken, selector_tags, detour_tags, state);
|
||||
|
||||
if (length(selector_tags) == 0)
|
||||
runtime_generate_unsupported("proxy section has no usable outbounds");
|
||||
|
||||
apply_detour_to_outbounds(config, selector_tags, outbound_detour_tag_for_section(section));
|
||||
apply_detour_to_outbounds(config, detour_tags, outbound_detour_tag_for_section(section));
|
||||
add_proxy_selector(config, section, selector_tags, state);
|
||||
if (!atomic_write_json_file(runtime_subscription.section_cache_path(section_name), state))
|
||||
runtime_generate_unsupported("failed to write section cache for " + section_name);
|
||||
|
||||
@@ -682,7 +682,6 @@ function init_config(populate_nft, caches_prepared, no_refresh) {
|
||||
if (reason == "")
|
||||
reason = "exit status " + generate_status;
|
||||
log_message("Failed to generate sing-box configuration: " + reason, "fatal");
|
||||
log_file_lines(runtime_log, "debug", "sing-box config generator: ");
|
||||
remove_files([ temp_config, runtime_log ]);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
@@ -198,10 +198,12 @@ function merge_source_metadata(state, section_name, source_section, source_index
|
||||
function remember_visible_outbound(state, tag_name, source_section, source_index, source_outbound_index, display_name, outbound) {
|
||||
if (type(state) != "object")
|
||||
return;
|
||||
state.linkRefs[tag_name] = {
|
||||
sourceSection: source_section,
|
||||
sourceIndex: source_outbound_index
|
||||
};
|
||||
let outbound_type = as_string(outbound.type || "");
|
||||
if (outbound_type != "selector" && outbound_type != "urltest")
|
||||
state.linkRefs[tag_name] = {
|
||||
sourceSection: source_section,
|
||||
sourceIndex: source_outbound_index
|
||||
};
|
||||
state.outboundMetadata.names[tag_name] = display_name;
|
||||
let server = as_string(outbound.server || "");
|
||||
if (server != "")
|
||||
|
||||
@@ -40,6 +40,12 @@ grep -Fq 'mode == "service-listen-address"' "$SINGBOX_RUNTIME_UC" ||
|
||||
fail "singbox/runtime.uc must own service listen address detection"
|
||||
grep -Fq 'mode == "service-proxy-address"' "$SINGBOX_RUNTIME_UC" ||
|
||||
fail "singbox/runtime.uc must own service proxy address detection"
|
||||
if grep -Fq 'log_file_lines(runtime_log, "debug", "sing-box config generator: ");' "$SINGBOX_RUNTIME_UC"; then
|
||||
fail "singbox/runtime.uc must not duplicate generator failure output as debug log"
|
||||
fi
|
||||
if grep -Fq 'warn("unsupported: "' "$SINGBOX_GENERATOR_UC"; then
|
||||
fail "singbox/generator.uc must emit concise generator failure reasons"
|
||||
fi
|
||||
if grep -n -E 'require\("uci"\)\.cursor|uci -q|uci", "-q"' "$SINGBOX_RUNTIME_UC" >/dev/null 2>&1; then
|
||||
fail "singbox/runtime.uc must use core.uci instead of owning direct UCI cursor or CLI calls"
|
||||
fi
|
||||
@@ -101,6 +107,30 @@ generate_config_with_subscription_cache() {
|
||||
"$fixture" "$output" "127.0.0.1" "0"
|
||||
}
|
||||
|
||||
cat >"$WORK_DIR/no-enabled-fixture.json" <<'JSON'
|
||||
{
|
||||
"settings": {
|
||||
".name": "settings",
|
||||
".type": "settings",
|
||||
"config_path": "/tmp/sing-box/config.json",
|
||||
"dns_server": "1.1.1.1",
|
||||
"service_listen_address": "127.0.0.1"
|
||||
},
|
||||
"section": []
|
||||
}
|
||||
JSON
|
||||
|
||||
if ucode -L "$PODKOP_LIB" "$SINGBOX_GENERATOR_UC" generate-config-fixture \
|
||||
"$WORK_DIR/no-enabled-fixture.json" "$WORK_DIR/no-enabled.json" "127.0.0.1" "0" \
|
||||
>"$WORK_DIR/no-enabled.stdout" 2>"$WORK_DIR/no-enabled.stderr"; then
|
||||
fail "generator should reject a config without enabled sections"
|
||||
fi
|
||||
if grep -Fq 'unsupported:' "$WORK_DIR/no-enabled.stderr"; then
|
||||
fail "generator failure reason must not include redundant unsupported prefix"
|
||||
fi
|
||||
grep -Fxq 'no enabled sections' "$WORK_DIR/no-enabled.stderr" ||
|
||||
fail "generator failure reason should be concise"
|
||||
|
||||
cat >"$WORK_DIR/generator-uci.state" <<'EOF_UCI'
|
||||
podkop-plus.settings=settings
|
||||
podkop-plus.settings.dns_server=1.1.1.1
|
||||
@@ -515,6 +545,40 @@ cat >"$WORK_DIR/subscription-metadata-fixture.json" <<'JSON'
|
||||
}
|
||||
JSON
|
||||
|
||||
cat >"$WORK_DIR/subscription-group-fixture.json" <<'JSON'
|
||||
{
|
||||
"settings": {
|
||||
".name": "settings",
|
||||
".type": "settings",
|
||||
"config_path": "/tmp/sing-box/config.json",
|
||||
"dns_server": "1.1.1.1",
|
||||
"service_listen_address": "127.0.0.1"
|
||||
},
|
||||
"section": [
|
||||
{
|
||||
".name": "detour",
|
||||
".type": "section",
|
||||
"enabled": "1",
|
||||
"action": "outbound",
|
||||
"outbound_json": "{\"type\":\"socks\",\"server\":\"127.0.0.1\",\"server_port\":1081}",
|
||||
"domain_suffix": [ "detour.example" ]
|
||||
},
|
||||
{
|
||||
".name": "grouped",
|
||||
".type": "section",
|
||||
"enabled": "1",
|
||||
"action": "proxy",
|
||||
"subscription_urls": [
|
||||
"https://example.com/group.json | Happ"
|
||||
],
|
||||
"outbound_detour_enabled": "1",
|
||||
"outbound_detour_section": "detour",
|
||||
"domain_suffix": [ "grouped.example" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON
|
||||
|
||||
mkdir -p "$WORK_DIR/subscriptions" "$WORK_DIR/persistent-subscription-cache"
|
||||
for source in proxy-subscription-1 proxy-subscription-2 test-subscription-1; do
|
||||
cat >"$WORK_DIR/subscriptions/$source.json" <<'JSON'
|
||||
@@ -532,6 +596,46 @@ printf '%s' 'v2rayN' >"$WORK_DIR/subscriptions/test-subscription-1.user_agent"
|
||||
printf '%s' 'v2rayN' >"$WORK_DIR/persistent-subscription-cache/proxy-subscription-1.user_agent"
|
||||
printf '%s' 'sing-box/default' >"$WORK_DIR/persistent-subscription-cache/proxy-subscription-2.user_agent"
|
||||
printf '%s' 'v2rayN' >"$WORK_DIR/persistent-subscription-cache/test-subscription-1.user_agent"
|
||||
cat >"$WORK_DIR/subscriptions/grouped-subscription-1.json" <<'JSON'
|
||||
{
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "urltest",
|
||||
"tag": "Provider Group",
|
||||
"outbounds": [ "grouped-out", "leaf" ],
|
||||
"url": "https://www.gstatic.com/generate_204",
|
||||
"interval": "10m",
|
||||
"tolerance": 50,
|
||||
"remark": "Provider Group",
|
||||
"__podkop_allow_group": true
|
||||
},
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "grouped-out",
|
||||
"server": "127.0.0.1",
|
||||
"server_port": 443,
|
||||
"uuid": "00000000-0000-4000-8000-000000000001",
|
||||
"tls": { "enabled": true, "server_name": "example.com" },
|
||||
"__podkop_hidden": true
|
||||
},
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "leaf",
|
||||
"server": "127.0.0.2",
|
||||
"server_port": 443,
|
||||
"uuid": "00000000-0000-4000-8000-000000000002",
|
||||
"tls": { "enabled": true, "server_name": "example.org" },
|
||||
"__podkop_hidden": true
|
||||
},
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "provider-direct"
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON
|
||||
printf '%s' 'https://example.com/group.json' >"$WORK_DIR/subscriptions/grouped-subscription-1.url"
|
||||
printf '%s' 'Happ' >"$WORK_DIR/subscriptions/grouped-subscription-1.user_agent"
|
||||
|
||||
generate_config "$WORK_DIR/disabled-updates-fixture.json" "$WORK_DIR/disabled.json"
|
||||
generate_config "$WORK_DIR/default-updates-fixture.json" "$WORK_DIR/default.json"
|
||||
@@ -547,6 +651,7 @@ generate_config "$WORK_DIR/mwan3-auto-fixture.json" "$WORK_DIR/mwan3-auto.json"
|
||||
generate_config "$WORK_DIR/mwan3-pinned-fixture.json" "$WORK_DIR/mwan3-pinned.json" 1
|
||||
generate_config "$WORK_DIR/domain-ip-rulesets-fixture.json" "$WORK_DIR/domain-ip-rulesets.json"
|
||||
generate_config_with_subscription_cache "$WORK_DIR/subscription-metadata-fixture.json" "$WORK_DIR/subscription-metadata.json"
|
||||
generate_config_with_subscription_cache "$WORK_DIR/subscription-group-fixture.json" "$WORK_DIR/subscription-group.json"
|
||||
|
||||
ucode -e '
|
||||
let fs = require("fs");
|
||||
@@ -743,6 +848,25 @@ for (let rule in generated_list.rules || []) {
|
||||
}
|
||||
assert(has_domain && has_ip, "generated domain/IP source ruleset contents");
|
||||
|
||||
let subscription_group = cfg("subscription-group");
|
||||
let provider_group = outbound(subscription_group, "Provider Group");
|
||||
assert(provider_group && provider_group.type == "urltest", "provider URLTest group imported");
|
||||
assert(length(provider_group.outbounds) == 2, "provider URLTest group keeps leaf references");
|
||||
assert(provider_group.outbounds[0] == "grouped-out-1", "provider URLTest group leaf reference retagged");
|
||||
assert(provider_group.outbounds[1] == "leaf", "provider URLTest group second leaf reference kept");
|
||||
assert(provider_group.detour == null, "provider URLTest group does not receive outbound detour");
|
||||
let grouped_leaf = outbound(subscription_group, "grouped-out-1");
|
||||
let second_leaf = outbound(subscription_group, "leaf");
|
||||
assert(grouped_leaf && grouped_leaf.detour == "detour-out", "hidden subscription leaf receives outbound detour");
|
||||
assert(second_leaf && second_leaf.detour == "detour-out", "second hidden subscription leaf receives outbound detour");
|
||||
assert(outbound(subscription_group, "provider-direct") == null, "provider direct outbound skipped");
|
||||
let grouped_selector = outbound(subscription_group, "grouped-out");
|
||||
assert(grouped_selector && length(grouped_selector.outbounds) == 1 && grouped_selector.outbounds[0] == "Provider Group", "selector exposes provider group only");
|
||||
let grouped_state = cfg("subscription-group.json.section-cache/grouped");
|
||||
assert(grouped_state.outboundMetadata.names["Provider Group"] == "Provider Group", "provider group metadata visible");
|
||||
assert(grouped_state.outboundMetadata.names["grouped-out-1"] == null, "hidden leaf metadata not visible");
|
||||
assert(grouped_state.linkRefs["Provider Group"] == null, "provider group has no source link ref");
|
||||
|
||||
let proxy_cache = json(fs.readfile(dir + "/subscription-metadata.json.section-cache/proxy.json"));
|
||||
let test_cache = json(fs.readfile(dir + "/subscription-metadata.json.section-cache/test.json"));
|
||||
let proxy_metadata = as_array(proxy_cache.subscriptionMetadata);
|
||||
|
||||
Reference in New Issue
Block a user