feat(ruleset): auto-assign download_detour for remote rulesets & add watchdog self-heal for ruleset DNS stalls

This commit is contained in:
Dushnilin
2026-08-30 21:22:23 +03:00
parent dfb32722bf
commit 706b35328c
6 changed files with 190 additions and 19 deletions
+25 -3
View File
@@ -3115,7 +3115,7 @@ function run_doctor_checks_impl(repair) {
doc_plan("/usr/bin/tachyon list_update"); doc_plan("/usr/bin/tachyon list_update");
doc_check("⚠️", "Community lists", doc_check("⚠️", "Community lists",
sprintf("%d/%d missing", length(missing_lists), length(all_required)), sprintf("%d/%d missing", length(missing_lists), length(all_required)),
"→ WILL FIX (doctor --fix): загрузка через list_update"); "→ WILL FIX (doctor --fix): загрузка через list_update (с прокси-маршрутизацией при необходимости)");
} else { } else {
// Attempt repair: download missing lists // Attempt repair: download missing lists
command_status("/usr/bin/tachyon list_update > /dev/null 2>&1"); command_status("/usr/bin/tachyon list_update > /dev/null 2>&1");
@@ -3129,15 +3129,37 @@ function run_doctor_checks_impl(repair) {
} }
} }
if (length(still_missing) > 0) {
let first_proxy = "";
for (let s in all_sections) {
if (s.enabled != "0" && s.action != "bypass" && s.action != "block" && s.action != "dns" && s.action != "") {
first_proxy = s[".name"];
break;
}
}
if (first_proxy != "") {
command_status("uci set tachyon.settings.download_lists_via_proxy='1'; uci set tachyon.settings.download_lists_via_proxy_section=" + shell_quote(first_proxy) + "; uci commit tachyon >/dev/null 2>&1");
command_status("/usr/bin/tachyon list_update > /dev/null 2>&1");
still_missing = [];
for (let list_name in missing_lists) {
let srs_path = TMP_RULESET_FOLDER + "/community-" + list_name + ".srs";
let srs_stat = fs.stat(srs_path);
if (srs_stat == null || srs_stat.size == 0) {
push(still_missing, list_name);
}
}
}
}
if (length(still_missing) == 0) { if (length(still_missing) == 0) {
doc_check("❌", "Community lists", doc_check("❌", "Community lists",
sprintf("%d/%d missing", length(missing_lists), length(all_required)), sprintf("%d/%d missing", length(missing_lists), length(all_required)),
"→ FIXED: загружены через list_update"); "→ FIXED: загружены через list_update (маршрут через прокси настроен)");
fixed++; fixed++;
} else { } else {
doc_check("❌", "Community lists", doc_check("❌", "Community lists",
sprintf("%d/%d отсутствуют: %s", length(still_missing), length(all_required), join(", ", still_missing)), sprintf("%d/%d отсутствуют: %s", length(still_missing), length(all_required), join(", ", still_missing)),
"→ не удалось загрузить — проверьте интернет-соединение"); "→ не удалось загрузить — проверьте доступность прокси/GitHub");
} }
} }
} }
+17 -13
View File
@@ -52,28 +52,32 @@ let proxy_port = "";
let proxy_interface = ""; let proxy_interface = "";
if (warp_proxy_section != "") { if (warp_proxy_section != "") {
if (uci.get(CONFIG_NAME, warp_proxy_section)) { let clean_section = warp_proxy_section;
let section_action = uci.get(CONFIG_NAME, warp_proxy_section, "action") || ""; if (match(clean_section, /^netif:/)) {
let section_iface = uci.get(CONFIG_NAME, warp_proxy_section, "section_interface") || clean_section = substr(clean_section, 6);
uci.get(CONFIG_NAME, warp_proxy_section, "interface") || }
uci.get(CONFIG_NAME, warp_proxy_section, "device") || ""; if (uci.get(CONFIG_NAME, clean_section)) {
let section_port = uci.get(CONFIG_NAME, warp_proxy_section, "mixed_proxy_port") || ""; let section_action = uci.get(CONFIG_NAME, clean_section, "action") || "";
let section_iface = uci.get(CONFIG_NAME, clean_section, "section_interface") ||
uci.get(CONFIG_NAME, clean_section, "interface") ||
uci.get(CONFIG_NAME, clean_section, "device") || "";
let section_port = uci.get(CONFIG_NAME, clean_section, "mixed_proxy_port") || "";
if (section_iface != "") { if (section_iface != "") {
proxy_interface = section_iface; proxy_interface = section_iface;
} else if (section_action == "interface") { } else if (section_action == "interface") {
proxy_interface = warp_proxy_section; proxy_interface = clean_section;
} }
if (section_port != "" && match(section_port, /^[0-9]+$/) != null) { if (section_port != "" && match(section_port, /^[0-9]+$/) != null) {
proxy_port = section_port; proxy_port = section_port;
} }
} else if (fs.stat("/sys/class/net/" + warp_proxy_section) != null) { } else if (fs.stat("/sys/class/net/" + clean_section) != null) {
proxy_interface = warp_proxy_section; proxy_interface = clean_section;
} else if (uci.get("network", warp_proxy_section)) { } else if (uci.get("network", clean_section)) {
let net_dev = uci.get("network", warp_proxy_section, "device") || let net_dev = uci.get("network", clean_section, "device") ||
uci.get("network", warp_proxy_section, "ifname") || uci.get("network", clean_section, "ifname") ||
warp_proxy_section; clean_section;
if (fs.stat("/sys/class/net/" + net_dev) != null) { if (fs.stat("/sys/class/net/" + net_dev) != null) {
proxy_interface = net_dev; proxy_interface = net_dev;
} }
+41 -2
View File
@@ -761,6 +761,44 @@ function heal_qos(ev) {
safe_reload_firewall(); safe_reload_firewall();
} }
function heal_uncached_rulesets_detour() {
let cfg = settings();
if (common.bool_option(cfg, "download_lists_via_proxy", false))
return false;
let has_rulesets = false;
let proxy_section_name = "";
uci_core.foreach(CONFIG_NAME, "section", function(s) {
if (!common.bool_option(s, "enabled", true)) return;
let act = common.option(s, "action", "");
if (act != "bypass" && act != "block" && act != "dns" && act != "") {
if (proxy_section_name == "")
proxy_section_name = s[".name"];
}
let lists = connections.community_lists(s);
let custom_rulesets = connections.rule_sets(s);
if (length(lists) > 0 || length(custom_rulesets) > 0)
has_rulesets = true;
});
if (has_rulesets && proxy_section_name != "") {
log_message("Watchdog: DNS stalled with remote rulesets on direct WAN; auto-enabling download_lists_via_proxy through " + proxy_section_name, "warn");
system("/sbin/uci set tachyon.settings.download_lists_via_proxy='1' >/dev/null 2>&1");
system("/sbin/uci set tachyon.settings.download_lists_via_proxy_section=" + shell_quote(proxy_section_name) + " >/dev/null 2>&1");
system("/sbin/uci commit tachyon >/dev/null 2>&1");
bg_system("/usr/bin/tachyon list_update");
ai_heal_report(
"ruleset_proxy_detour",
"DNS stalled during remote ruleset fetch on direct WAN",
"Включена загрузка списков через прокси-секцию " + proxy_section_name + " и запущено фоновое обновление",
"fixed"
);
return true;
}
return false;
}
// Threshold 3 is the original ai_heal_dns() streak: two isolated failures are // Threshold 3 is the original ai_heal_dns() streak: two isolated failures are
// noise, three in a row is a stall. The streak reset after acting prevents the // noise, three in a row is a stall. The streak reset after acting prevents the
// in-flight restart from tripping the same threshold on the next tick. // in-flight restart from tripping the same threshold on the next tick.
@@ -771,12 +809,13 @@ function heal_dns_stall(ev) {
controller.reset_dns_streak(); controller.reset_dns_streak();
let detour_healed = heal_uncached_rulesets_detour();
log_message("Watchdog: DNS stalled after 3 attempts, soft-reloading proxy runtime", "warn"); log_message("Watchdog: DNS stalled after 3 attempts, soft-reloading proxy runtime", "warn");
let incident = { let incident = {
type: "dns", type: "dns",
description: "DNS resolution stalled on sing-box (port 53)", description: detour_healed ? "Зависание DNS из-за блокировки прямой загрузки списков правил" : "DNS resolution stalled on sing-box (port 53)",
resolution: "Soft-reloaded proxy runtime safely without breaking active TCP/RDP connections" resolution: detour_healed ? "Включена загрузка списков через прокси и выполнен мягкий перезапуск" : "Soft-reloaded proxy runtime safely without breaking active TCP/RDP connections"
}; };
// The restart is asynchronous, so `true` means only that it was launched. // The restart is asynchronous, so `true` means only that it was launched.
+12 -1
View File
@@ -326,7 +326,18 @@ function download_via_proxy_section(settings, purpose) {
if (configured != "") if (configured != "")
return configured; return configured;
return option(settings, "download_lists_via_proxy_section", ""); let fallback_configured = option(settings, "download_lists_via_proxy_section", "");
if (fallback_configured != "")
return fallback_configured;
if (type(enabled_sections) == "function") {
for (let s in enabled_sections()) {
let act = option(s, "action", "");
if (act != "bypass" && act != "block" && act != "dns" && act != "")
return s[".name"];
}
}
return "";
} }
function download_via_proxy_enabled(settings, purpose) { function download_via_proxy_enabled(settings, purpose) {
@@ -790,6 +790,12 @@ function ensure_community_ruleset(config, section_name, community) {
update_interval: remote_ruleset_update_interval() update_interval: remote_ruleset_update_interval()
}; };
let detour = ctx.download_detour_tag(ctx.runtime_settings(), "lists"); let detour = ctx.download_detour_tag(ctx.runtime_settings(), "lists");
if (detour == "" && section_name != "") {
let sec_out = outbound_tag(section_name);
if (sec_out != "" && sec_out != runtime_constants.DIRECT_OUTBOUND_TAG && sec_out != runtime_constants.BYPASS_OUTBOUND_TAG) {
detour = sec_out;
}
}
if (detour != "") if (detour != "")
rule_set.download_detour = detour; rule_set.download_detour = detour;
push(config.route.rule_set, rule_set); push(config.route.rule_set, rule_set);
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
set -eo pipefail
TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$TEST_DIR/.." && pwd)"
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
GENERATOR_UC="$TACHYON_LIB/singbox/generator.uc"
WORK_DIR="$(mktemp -d)"
trap 'rm -rf "$WORK_DIR"' EXIT
fail() {
echo "FAIL: $*" >&2
exit 1
}
# 1. Verify remote rule_set gets download_detour assigned to section's proxy outbound
cat >"$WORK_DIR/fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"enabled": "1",
"dns_type": "udp",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "myproxy",
".type": "section",
"enabled": "1",
"action": "connection",
"outbound_jsons": [ "{\"type\":\"direct\"}" ],
"community_lists": [ "google_ai" ]
}
]
}
JSON
output="$WORK_DIR/out.json"
mkdir -p "$output.section-cache" "$output.rulesets"
ucode -L "$TACHYON_LIB" "$GENERATOR_UC" generate-config-fixture \
"$WORK_DIR/fixture.json" "$output" "127.0.0.1" "0" "1"
grep -Fq '"download_detour": "myproxy-out"' "$output" || \
fail "remote community ruleset must automatically get download_detour set to section outbound"
# 2. Verify download_via_proxy fallback selects first enabled proxy section
cat >"$WORK_DIR/fixture-detour-enabled.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"enabled": "1",
"dns_type": "udp",
"dns_server": "1.1.1.1",
"download_lists_via_proxy": "1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "first_proxy",
".type": "section",
"enabled": "1",
"action": "connection",
"outbound_jsons": [ "{\"type\":\"direct\"}" ]
},
{
".name": "other_sec",
".type": "section",
"enabled": "1",
"action": "connection",
"outbound_jsons": [ "{\"type\":\"direct\"}" ],
"community_lists": [ "google_ai" ]
}
]
}
JSON
output2="$WORK_DIR/out2.json"
mkdir -p "$output2.section-cache" "$output2.rulesets"
ucode -L "$TACHYON_LIB" "$GENERATOR_UC" generate-config-fixture \
"$WORK_DIR/fixture-detour-enabled.json" "$output2" "127.0.0.1" "0" "1"
grep -Fq '"download_detour": "first_proxy-out"' "$output2" || \
fail "global download_lists_via_proxy without explicit section must default to first enabled proxy section"
printf "ruleset download detour checks passed\n"