feat(ruleset): auto-assign download_detour for remote rulesets & add watchdog self-heal for ruleset DNS stalls
This commit is contained in:
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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"
|
||||||
Reference in New Issue
Block a user