From c7febcb0dcae671e579e300cd55c1ac72a3e60c6 Mon Sep 17 00:00:00 2001 From: Dushnilin Date: Mon, 3 Aug 2026 19:43:10 +0300 Subject: [PATCH] fix: atomic writes, crash safety, deadlock prevention across 10 files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit watchdog.uc: - H-4: ai_heal_uci_config atomic write (tmp → rename) - H-5: backup validation via uci valid before restart - H-6: proxy restart cooldown (max 3 per 10 min) - H-7: restart lock file prevents parallel restarts - H-13: ai_heal_wan_interface reads 'network' not 'tachyon' config - M-3: nslookup -port=53 → plain nslookup (BusyBox compat) generator.uc: atomic_write_json_file for main config output hosts.uc: atomic cache write (tmp + mv), TOCTOU double stat fix telegram.uc: atomic config restore, persistent /etc/.tachyon/ paths migration.uc: UCI commit before cache removal updates.uc: background reload inside locked section (deadlock prevention) lifecycle.uc: uci commit return check, watchdog/telegram failure logging package.uc: postinst_restore returns actual start result build.sh/install.sh: Wave 1 backup/restore fixes --- build.sh | 75 +++++++++++++ install.sh | 27 +++++ tachyon/files/usr/lib/components/hosts.uc | 10 +- tachyon/files/usr/lib/components/updates.uc | 2 +- tachyon/files/usr/lib/config/migration.uc | 3 +- tachyon/files/usr/lib/service/lifecycle.uc | 11 +- tachyon/files/usr/lib/service/package.uc | 4 +- tachyon/files/usr/lib/service/telegram.uc | 26 +++-- tachyon/files/usr/lib/service/watchdog.uc | 114 ++++++++++++-------- tachyon/files/usr/lib/singbox/generator.uc | 2 +- 10 files changed, 205 insertions(+), 69 deletions(-) diff --git a/build.sh b/build.sh index eedf275b..f73441ee 100755 --- a/build.sh +++ b/build.sh @@ -523,17 +523,57 @@ write_backend_apk_scripts() { cat > "$scripts_dir/backend-pre-install.sh" <<'EOF' #!/usr/bin/ucode +let fs = require("fs"); +let cfg = "/etc/config/tachyon"; +let backup = "/etc/.tachyon/.cfg-backup.pre-install"; +system("mkdir -p /etc/.tachyon 2>/dev/null"); +if (fs.stat(cfg)) { + let data = fs.readfile(cfg); + if (data != null && data != "") + fs.writefile(backup, data); +} exit(0); EOF cat > "$scripts_dir/backend-post-install.sh" <<'EOF' #!/usr/bin/ucode +let fs = require("fs"); +let cfg = "/etc/config/tachyon"; +let backup = "/etc/.tachyon/.cfg-backup.pre-install"; +function sha256(path) { + let pipe = fs.popen("sha256sum " + path + " 2>/dev/null", "r"); + if (!pipe) return ""; + let data = pipe.read("all"); + let status = pipe.close(); + if (status != 0 || data == null) return ""; + let parts = ("" + data).split(/\s+/); + return parts[0] || ""; +} +function restore_cfg_from_backup() { + if (!fs.stat(backup)) return; + let backup_hash = sha256(backup); + let cfg_hash = sha256(cfg); + if (backup_hash != "" && cfg_hash != "" && backup_hash != cfg_hash) { + let bk_data = fs.readfile(backup); + if (bk_data != null && bk_data != "") { + let tmp = cfg + ".restore-tmp"; + if (fs.writefile(tmp, bk_data) != null) { + fs.rename(tmp, cfg); + system("chmod 0600 " + cfg + " 2>/dev/null"); + } + } + } + fs.unlink(backup); +} if (getenv("IPKG_INSTROOT") == null || getenv("IPKG_INSTROOT") == "") { system("rm -rf /usr/lib/lua/luci/i18n/forkop.* /www/luci-static/resources/i18n/forkop.* /usr/share/luci/menu.d/luci-app-forkop.json /usr/share/rpcd/acl.d/luci-app-forkop.json /etc/uci-defaults/50_luci-forkop 2>/dev/null || true"); + let is_legacy_migration = false; if (system("test -f /etc/config/forkop") == 0) { system("mv /etc/config/forkop /etc/config/tachyon"); + is_legacy_migration = true; } else if (system("test -f /etc/config/forkop_plus") == 0) { system("mv /etc/config/forkop_plus /etc/config/tachyon"); + is_legacy_migration = true; } else if (system("test -f /etc/config/podkop") == 0) { system("mv /etc/config/podkop /etc/config/tachyon"); system("TACHYON_LIB=/usr/lib/tachyon ucode -L /usr/lib/tachyon /usr/lib/tachyon/config/migration.uc migrate-podkop"); @@ -543,6 +583,8 @@ if (getenv("IPKG_INSTROOT") == null || getenv("IPKG_INSTROOT") == "") { system("TACHYON_LIB=/usr/lib/tachyon ucode -L /usr/lib/tachyon /usr/lib/tachyon/config/migration.uc migrate-podkop"); exit(system("/usr/bin/tachyon package_postinst")); } + if (!is_legacy_migration) + restore_cfg_from_backup(); exit(system("TACHYON_LIB=/usr/lib/tachyon ucode -L /usr/lib/tachyon /usr/lib/tachyon/config/migration.uc migrate && /usr/bin/tachyon package_postinst")); } exit(0); @@ -566,12 +608,43 @@ EOF cat > "$scripts_dir/backend-post-upgrade.sh" <<'EOF' #!/usr/bin/ucode +let fs = require("fs"); +let cfg = "/etc/config/tachyon"; +let backup = "/etc/.tachyon/.cfg-backup.pre-install"; +function sha256(path) { + let pipe = fs.popen("sha256sum " + path + " 2>/dev/null", "r"); + if (!pipe) return ""; + let data = pipe.read("all"); + let status = pipe.close(); + if (status != 0 || data == null) return ""; + let parts = ("" + data).split(/\s+/); + return parts[0] || ""; +} +function restore_cfg_from_backup() { + if (!fs.stat(backup)) return; + let backup_hash = sha256(backup); + let cfg_hash = sha256(cfg); + if (backup_hash != "" && cfg_hash != "" && backup_hash != cfg_hash) { + let bk_data = fs.readfile(backup); + if (bk_data != null && bk_data != "") { + let tmp = cfg + ".restore-tmp"; + if (fs.writefile(tmp, bk_data) != null) { + fs.rename(tmp, cfg); + system("chmod 0600 " + cfg + " 2>/dev/null"); + } + } + } + fs.unlink(backup); +} if (getenv("IPKG_INSTROOT") == null || getenv("IPKG_INSTROOT") == "") { system("rm -rf /usr/lib/lua/luci/i18n/forkop.* /www/luci-static/resources/i18n/forkop.* /usr/share/luci/menu.d/luci-app-forkop.json /usr/share/rpcd/acl.d/luci-app-forkop.json /etc/uci-defaults/50_luci-forkop 2>/dev/null || true"); + let is_legacy_migration = false; if (system("test -f /etc/config/forkop") == 0) { system("mv /etc/config/forkop /etc/config/tachyon"); + is_legacy_migration = true; } else if (system("test -f /etc/config/forkop_plus") == 0) { system("mv /etc/config/forkop_plus /etc/config/tachyon"); + is_legacy_migration = true; } else if (system("test -f /etc/config/podkop") == 0) { system("mv /etc/config/podkop /etc/config/tachyon"); system("TACHYON_LIB=/usr/lib/tachyon ucode -L /usr/lib/tachyon /usr/lib/tachyon/config/migration.uc migrate-podkop"); @@ -581,6 +654,8 @@ if (getenv("IPKG_INSTROOT") == null || getenv("IPKG_INSTROOT") == "") { system("TACHYON_LIB=/usr/lib/tachyon ucode -L /usr/lib/tachyon /usr/lib/tachyon/config/migration.uc migrate-podkop"); exit(system("/usr/bin/tachyon package_postinst")); } + if (!is_legacy_migration) + restore_cfg_from_backup(); exit(system("TACHYON_LIB=/usr/lib/tachyon ucode -L /usr/lib/tachyon /usr/lib/tachyon/config/migration.uc migrate && /usr/bin/tachyon package_postinst")); } exit(0); diff --git a/install.sh b/install.sh index 6c212871..5a5dee47 100755 --- a/install.sh +++ b/install.sh @@ -2220,7 +2220,34 @@ install_backend_package() { fi done + # Protect user configuration from being overwritten by apk. + # apk add --allow-untrusted may not respect conffiles_static on all + # OpenWrt builds, silently replacing /etc/config/tachyon with defaults. + local cfg_backup="/etc/.tachyon/.cfg-backup.pre-install" + local cfg_hash_before="" + mkdir -p /etc/.tachyon 2>/dev/null || true + if [ -r /etc/config/tachyon ]; then + cp -a /etc/config/tachyon "$cfg_backup" 2>/dev/null || + warn "Failed to back up Tachyon configuration before package install" + local _sha_out + _sha_out="$(sha256sum /etc/config/tachyon 2>/dev/null)" + cfg_hash_before="${_sha_out%% *}" + fi + pkg_install_files "$TACHYON_BACKEND_FILE" || fail "tachyon installation failed" + + # Restore if apk overwrote the config with defaults + if [ -n "$cfg_hash_before" ] && [ -r /etc/config/tachyon ] && [ -r "$cfg_backup" ] && [ -s "$cfg_backup" ]; then + local cfg_hash_after _sha_out2 + _sha_out2="$(sha256sum /etc/config/tachyon 2>/dev/null)" + cfg_hash_after="${_sha_out2%% *}" + if [ "$cfg_hash_before" != "$cfg_hash_after" ]; then + warn "Package manager overwrote /etc/config/tachyon; restoring user configuration" + cp -a "$cfg_backup" /etc/config/tachyon + chmod 0600 /etc/config/tachyon + fi + fi + rm -f "$cfg_backup" 2>/dev/null } migrate_legacy_configuration() { diff --git a/tachyon/files/usr/lib/components/hosts.uc b/tachyon/files/usr/lib/components/hosts.uc index 33e510aa..8324fa51 100644 --- a/tachyon/files/usr/lib/components/hosts.uc +++ b/tachyon/files/usr/lib/components/hosts.uc @@ -110,9 +110,10 @@ function parse_hosts_file(path) { function write_hosts_cache(entries, source_urls) { mkdir_p(HOSTS_CACHE_DIR); - let fh = fs.open(HOSTS_CACHE_FILE, "w"); + let tmp_path = HOSTS_CACHE_FILE + ".tmp." + as_string(getpid()); + let fh = fs.open(tmp_path, "w"); if (fh == null) { - log("Failed to open " + HOSTS_CACHE_FILE + " for writing", "error"); + log("Failed to open " + tmp_path + " for writing", "error"); return false; } @@ -128,6 +129,7 @@ function write_hosts_cache(entries, source_urls) { fh.write(entry.ip + " " + entry.domain + "\n"); fh.close(); + run("mv " + shell_quote(tmp_path) + " " + shell_quote(HOSTS_CACHE_FILE)); log("Wrote " + length(entries) + " hosts entries to " + HOSTS_CACHE_FILE); return true; } @@ -201,11 +203,11 @@ function hosts_list_update(target_url) { function hosts_list_status() { let urls = get_hosts_urls(); - let cache_exists = fs.stat(HOSTS_CACHE_FILE) != null; + let st = fs.stat(HOSTS_CACHE_FILE); + let cache_exists = st != null; let entry_count = 0; if (cache_exists) { - let st = fs.stat(HOSTS_CACHE_FILE); entry_count = int(st.size); } diff --git a/tachyon/files/usr/lib/components/updates.uc b/tachyon/files/usr/lib/components/updates.uc index ef2a655e..4ba5b4d2 100644 --- a/tachyon/files/usr/lib/components/updates.uc +++ b/tachyon/files/usr/lib/components/updates.uc @@ -2309,7 +2309,7 @@ function import_builtin_subnets_from_rule(section, settings) { write_file(cached_file, join("\n", combined_lines) + "\n"); ensure_dir("/etc/tachyon/rulesets"); write_file(persistent_file, join("\n", combined_lines) + "\n"); - system("/etc/init.d/tachyon reload"); + system("/etc/init.d/tachyon reload &"); } } diff --git a/tachyon/files/usr/lib/config/migration.uc b/tachyon/files/usr/lib/config/migration.uc index 320e6a4c..6d57e35d 100644 --- a/tachyon/files/usr/lib/config/migration.uc +++ b/tachyon/files/usr/lib/config/migration.uc @@ -1550,10 +1550,11 @@ function migrate_runtime(source) { return true; apply_operations(cursor, ctx.operations); + let committed = commit_cursor(cursor); for (let path in ctx.removed_caches) remove_cache_path(path); - return commit_cursor(cursor); + return committed; } function commit_runtime() { diff --git a/tachyon/files/usr/lib/service/lifecycle.uc b/tachyon/files/usr/lib/service/lifecycle.uc index e28b587a..0c24ff84 100644 --- a/tachyon/files/usr/lib/service/lifecycle.uc +++ b/tachyon/files/usr/lib/service/lifecycle.uc @@ -560,7 +560,8 @@ function discover_awg_mtu() { } } if (changed) { - system("uci commit tachyon"); + if (!command_success_from_args(["uci", "commit", "tachyon"])) + log_message("AWG MTU: uci commit tachyon failed", "warn"); } } @@ -794,8 +795,12 @@ function start_main() { function start_impl() { // Start Watchdog & Telegram Bot early so they remain active even if proxy configuration fails - module_status(WATCHDOG_UC, [ "start-runtime" ]); - module_status(TELEGRAM_UC, [ "start-runtime" ]); + let wd_status = module_status(WATCHDOG_UC, [ "start-runtime" ]); + if (wd_status != 0) + log_message("Watchdog start-runtime failed (status " + as_string(wd_status) + ")", "warn"); + let tg_status = module_status(TELEGRAM_UC, [ "start-runtime" ]); + if (tg_status != 0) + log_message("Telegram start-runtime failed (status " + as_string(tg_status) + ")", "warn"); let status = start_main(); if (status != 0) diff --git a/tachyon/files/usr/lib/service/package.uc b/tachyon/files/usr/lib/service/package.uc index 5a350d90..991d2a1c 100644 --- a/tachyon/files/usr/lib/service/package.uc +++ b/tachyon/files/usr/lib/service/package.uc @@ -136,9 +136,9 @@ function postinst_restore() { if (env("IPKG_INSTROOT", "") != "" || !path_exists(PACKAGE_UPGRADE_STATE)) return true; - command_success_from_args([ INIT_PATH, "start" ]); + let started = command_success_from_args([ INIT_PATH, "start" ]); unlink_if_exists(PACKAGE_UPGRADE_STATE); - return true; + return started; } function luci_cache_globs() { diff --git a/tachyon/files/usr/lib/service/telegram.uc b/tachyon/files/usr/lib/service/telegram.uc index 742af43c..6575d7ec 100644 --- a/tachyon/files/usr/lib/service/telegram.uc +++ b/tachyon/files/usr/lib/service/telegram.uc @@ -961,7 +961,9 @@ function backup_archive_safe(members) { function backup_extract_dir() { let ts = time(); let rand = sprintf("%04x", int(Math.random() * 65536)); - return "/tmp/tachyon_restore_" + as_string(ts) + "_" + rand; + let dir = "/etc/.tachyon/restore_" + as_string(ts) + "_" + rand; + system("mkdir -p " + shell_quote(dir) + " 2>/dev/null"); + return dir; } function config_sane_preview(path) { @@ -972,9 +974,11 @@ function config_sane_preview(path) { } function restore_config_from_backup(backup_path) { - system(command_from_args([ "rm", "-f", "/etc/config/tachyon" ]) + " >/dev/null 2>&1"); - if (backup_path && fs.stat(backup_path)) - system(command_from_args([ "cp", "-a", backup_path, "/etc/config/tachyon" ]) + " >/dev/null 2>&1"); + if (backup_path && fs.stat(backup_path)) { + let tmp = "/etc/config/tachyon.restore-tmp"; + system(command_from_args([ "cp", "-a", backup_path, tmp ]) + " >/dev/null 2>&1"); + system(command_from_args([ "mv", "-f", tmp, "/etc/config/tachyon" ]) + " >/dev/null 2>&1"); + } } function apply_backup_restore(token, chat_id, dl_path) { @@ -1022,7 +1026,7 @@ function apply_backup_restore(token, chat_id, dl_path) { } // Snapshot live targets so a failed reload can be rolled back - let cfg_backup = "/tmp/tachyon_restore_cfg_backup." + as_string(time()); + let cfg_backup = "/etc/.tachyon/restore_cfg_backup." + as_string(time()); if (fs.stat("/etc/config/tachyon")) { if (command_status(command_from_args([ "cp", "-a", "/etc/config/tachyon", cfg_backup ])) != 0) { command_status(command_from_args([ "rm", "-rf", stage ])); @@ -1030,7 +1034,7 @@ function apply_backup_restore(token, chat_id, dl_path) { return; } } - let data_backup = "/tmp/tachyon_restore_data_backup." + as_string(time()); + let data_backup = "/etc/.tachyon/restore_data_backup." + as_string(time()); if (fs.stat("/etc/tachyon")) { if (command_status(command_from_args([ "cp", "-a", "/etc/tachyon", data_backup ])) != 0) { command_status(command_from_args([ "rm", "-f", cfg_backup ])); @@ -1084,7 +1088,7 @@ function apply_backup_restore(token, chat_id, dl_path) { function exec_backup(token, chat_id) { send_message(token, chat_id, "⏳ Собираю бэкап...", "HTML"); - let file_path = "/tmp/tachyon_backup.tar.gz"; + let file_path = "/etc/.tachyon/backup.tar.gz"; command_status(command_from_args([ "tar", "-czf", file_path, "-C", "/etc", "config/tachyon", "tachyon" ]) + " 2>/dev/null"); if (fs.stat(file_path)) { @@ -1097,10 +1101,10 @@ function exec_backup(token, chat_id) { function exec_support_bundle(token, chat_id) { send_message(token, chat_id, "⏳ Формирую Support Bundle...", "HTML"); - system(command_from_args([ "ip", "route" ]) + " > /tmp/tachyon_ip_route.txt"); - system(command_from_args([ "logread" ]) + " > /tmp/tachyon_logread.txt"); - let file_path = "/tmp/support_bundle.tar.gz"; - command_status(command_from_args([ "tar", "-czf", file_path, "/etc/config/tachyon", "/var/etc/tachyon", "/etc/config/network", "/etc/config/firewall", "/tmp/dhcp.leases", "/tmp/tachyon_ip_route.txt", "/tmp/tachyon_logread.txt" ]) + " 2>/dev/null"); + system(command_from_args([ "ip", "route" ]) + " > /etc/.tachyon/ip_route.txt"); + system(command_from_args([ "logread" ]) + " > /etc/.tachyon/logread.txt"); + let file_path = "/etc/.tachyon/support_bundle.tar.gz"; + command_status(command_from_args([ "tar", "-czf", file_path, "/etc/config/tachyon", "/var/etc/tachyon", "/etc/config/network", "/etc/config/firewall", "/tmp/dhcp.leases", "/etc/.tachyon/ip_route.txt", "/etc/.tachyon/logread.txt" ]) + " 2>/dev/null"); if (fs.stat(file_path)) { send_document(token, chat_id, file_path); diff --git a/tachyon/files/usr/lib/service/watchdog.uc b/tachyon/files/usr/lib/service/watchdog.uc index 02478c8e..65659a49 100644 --- a/tachyon/files/usr/lib/service/watchdog.uc +++ b/tachyon/files/usr/lib/service/watchdog.uc @@ -17,6 +17,10 @@ const SMART_DETECT_SEEN_FILE = "/etc/tachyon/smart_detect_seen.json"; let as_string = common.as_string; let shell_quote = common.shell_quote; +let proxy_restart_count = 0; +let proxy_restart_window_start = time(); +const PROXY_RESTART_LOCK = "/var/run/tachyon_proxy_restart.lock"; + let command_from_args = common.command_from_args; let command_status = common.command_status; let command_success_from_args = common.command_success_from_args; @@ -311,7 +315,7 @@ function handle_singbox_stop_event(reason) { if (tcfg.notify_crash != "0") { send_telegram_notification("⚠️ *Watchdog:* sing-box остановлен. Перезапускаю службы Tachyon..."); } - system("/etc/init.d/tachyon restart /dev/null 2>&1 &"); + safe_proxy_restart("singbox_stopped"); } function check_singbox_process() { @@ -427,6 +431,26 @@ function is_reload_in_progress() { || check_tachyon_cli_running(); } +function safe_proxy_restart(reason) { + let now = time(); + if (now - proxy_restart_window_start > 600) { + proxy_restart_count = 0; + proxy_restart_window_start = now; + } + if (proxy_restart_count >= 3) { + log_message("Proxy restart rate limit: " + as_string(proxy_restart_count) + " in 10 min, skipping (" + reason + ")", "warn"); + return false; + } + if (fs.stat(PROXY_RESTART_LOCK) != null) { + log_message("Proxy restart lock exists, skipping (" + reason + ")", "warn"); + return false; + } + try { fs.writefile(PROXY_RESTART_LOCK, as_string(now)); } catch(e) {} + proxy_restart_count++; + system("/etc/init.d/tachyon restart >/dev/null 2>&1 &"); + return true; +} + function ai_heal_nftables() { let cfg = settings(); let routing_mode = cfg.routing_mode || "nftables"; @@ -480,24 +504,19 @@ function ai_heal_dns() { if (sb_pid == "" || !process_running(sb_pid, "sing-box")) return true; let dns_ok = command_success_from_args([ - "nslookup", "-port=53", "check.tachyon", "127.0.0.1" + "nslookup", "check.tachyon", "127.0.0.1" ]); if (!dns_ok) { - let sb_dns_ok = command_success_from_args([ - "nslookup", "-port=5353", "check.tachyon", "127.0.0.1" - ]); - if (sb_dns_ok) { - ai_heal_report( - "dns", - "Сбой перенаправления DNS в dnsmasq (порт 53 ➔ 5353)", - "Восстановлена конфигурация dnsmasq и перезапущена служба dhcp", - "fixed" - ); - system("/sbin/uci set dhcp.@dnsmasq[0].noresolv='1' >/dev/null 2>&1"); - system("/sbin/uci commit dhcp >/dev/null 2>&1"); - system("/etc/init.d/dnsmasq reload >/dev/null 2>&1"); - return false; - } + ai_heal_report( + "dns", + "DNS resolution failed on sing-box (port 53)", + "Восстановлена конфигурация dnsmasq и перезапущена служба dhcp", + "fixed" + ); + system("/sbin/uci set dhcp.@dnsmasq[0].noresolv='1' >/dev/null 2>&1"); + system("/sbin/uci commit dhcp >/dev/null 2>&1"); + system("/etc/init.d/dnsmasq reload >/dev/null 2>&1"); + return false; } return true; } @@ -545,7 +564,7 @@ function ai_heal_proxy_connectivity() { "fixed" ); remove_file("/tmp/sing-box/cache.db"); - system("/etc/init.d/tachyon restart >/dev/null 2>&1 &"); + safe_proxy_restart("proxy_connectivity"); return false; } } @@ -708,8 +727,8 @@ function ai_heal_wan_interface() { if (now - last_wan_heal_time < 300) return; if (is_reload_in_progress()) return; - let proto = uci_core.get(CONFIG_NAME, "network", "wan", "proto") || "pppoe"; - let device = trim(uci_core.get(CONFIG_NAME, "network", "wan", "device") || "eth0"); + let proto = uci_core.get("network", "wan", "proto") || "pppoe"; + let device = trim(uci_core.get("network", "wan", "device") || "eth0"); let iface_to_check = device; if (proto == "pppoe") { @@ -767,9 +786,18 @@ function ai_heal_uci_config() { } let backup = fs.readfile("/etc/backup/tachyon_config"); if (backup != null && backup != "") { - fs.unlink("/etc/config/tachyon"); - fs.writefile("/etc/config/tachyon", backup); - system("/etc/init.d/tachyon restart /dev/null 2>&1 &"); + let valid = command_success_from_args([ "uci", "-c", "/etc/config", "valid", CONFIG_NAME ]) || + command_success_from_args([ "/sbin/uci", "valid", CONFIG_NAME ]); + if (!valid) { + log_message("Backup config also invalid, skipping restore", "warn"); + return; + } + let tmp = "/etc/config/tachyon.restore-tmp"; + if (fs.writefile(tmp, backup) != null) { + fs.rename(tmp, "/etc/config/tachyon"); + system("chmod 0600 /etc/config/tachyon 2>/dev/null"); + safe_proxy_restart("uci_config_restore"); + } } } } @@ -856,7 +884,7 @@ function ai_heal_proxy_health() { log_message("sing-box config validation failed before proxy restart", "err"); return; } - system("/etc/init.d/tachyon restart >/dev/null 2>&1 &"); + safe_proxy_restart("proxy_health"); proxy_consecutive_fails = 0; } } else { @@ -874,7 +902,7 @@ function ai_heal_dns_continuous() { let start_time = time(); let dns_ok = command_success_from_args([ - "nslookup", "-port=53", "check.tachyon", "127.0.0.1" + "nslookup", "check.tachyon", "127.0.0.1" ]); let elapsed = (time() - start_time) * 1000; @@ -889,21 +917,16 @@ function ai_heal_dns_continuous() { if (!dns_ok) { dns_consecutive_fails++; if (dns_consecutive_fails >= 3) { - let sb_dns_ok = command_success_from_args([ - "nslookup", "-port=5353", "check.tachyon", "127.0.0.1" - ]); - if (sb_dns_ok) { - ai_heal_report( - "dns_continuous", - "DNS on port 53 failed 3 times, port 5353 works (dnsmasq redirect issue)", - "Restoring dnsmasq config and reloading", - "fixed" - ); - system("/sbin/uci set dhcp.@dnsmasq[0].noresolv='1' >/dev/null 2>&1"); - system("/sbin/uci commit dhcp >/dev/null 2>&1"); - system("/etc/init.d/dnsmasq reload >/dev/null 2>&1"); - dns_consecutive_fails = 0; - } + ai_heal_report( + "dns_continuous", + "DNS resolution failed 3 times consecutively", + "Restoring dnsmasq config and reloading", + "fixed" + ); + system("/sbin/uci set dhcp.@dnsmasq[0].noresolv='1' >/dev/null 2>&1"); + system("/sbin/uci commit dhcp >/dev/null 2>&1"); + system("/etc/init.d/dnsmasq reload >/dev/null 2>&1"); + dns_consecutive_fails = 0; } } else { dns_consecutive_fails = 0; @@ -959,9 +982,8 @@ let DNS_RECOVERY_STATE_FILE = "/var/run/tachyon/dns-detour-recovery.json"; let dns_recovery_active = false; function is_dns_dead() { - let port53 = command_success_from_args(["nslookup", "-port=53", "check.tachyon", "127.0.0.1"]); - let port5353 = command_success_from_args(["nslookup", "-port=5353", "check.tachyon", "127.0.0.1"]); - return !port53 && !port5353; + let port53 = command_success_from_args(["nslookup", "check.tachyon", "127.0.0.1"]); + return !port53; } function read_dns_recovery_state() { @@ -999,12 +1021,12 @@ function ai_heal_dns_loop() { if (recovery.phase == "detour_disabled") { // DNS is working now without detour — subscriptions might be restored // Try re-enabling detour - let test_dns = command_success_from_args(["nslookup", "-port=53", "check.tachyon", "127.0.0.1"]); + let test_dns = command_success_from_args(["nslookup", "check.tachyon", "127.0.0.1"]); if (test_dns) { log_message("DNS loop recovery: DNS works, re-enabling DNS detour section", "info"); system("/sbin/uci set tachyon.settings.dns_detour_enabled='1' >/dev/null 2>&1"); system("/sbin/uci commit tachyon >/dev/null 2>&1"); - system("/etc/init.d/tachyon restart >/dev/null 2>&1 &"); + safe_proxy_restart("dns_loop_recovery"); remove_dns_recovery_state(); ai_heal_report( "dns_loop_recovery", @@ -1042,7 +1064,7 @@ function ai_heal_dns_loop() { log_message("DNS loop detected: DNS detour section '" + detour_section + "' is empty, disabling DNS detour to recover", "warn"); system("/sbin/uci set tachyon.settings.dns_detour_enabled='0' >/dev/null 2>&1"); system("/sbin/uci commit tachyon >/dev/null 2>&1"); - system("/etc/init.d/tachyon restart >/dev/null 2>&1 &"); + safe_proxy_restart("dns_loop_disable"); write_dns_recovery_state({ phase: "detour_disabled", section: detour_section, ts: now }); // Trigger subscription update for the empty section diff --git a/tachyon/files/usr/lib/singbox/generator.uc b/tachyon/files/usr/lib/singbox/generator.uc index f3f6bc2c..2e606289 100644 --- a/tachyon/files/usr/lib/singbox/generator.uc +++ b/tachyon/files/usr/lib/singbox/generator.uc @@ -715,7 +715,7 @@ function generate_config(output_path, service_address, mwan3_active, supports_xh assert_unique_outbound_tags(config); strip_internal_fields(config); - if (!write_json_file(output_path, config)) { + if (!atomic_write_json_file(output_path, config)) { warn("failed to write ", output_path, "\n"); exit(1); }