fix: комплексный аудит и исправление ucode кодовой базы

Критические исправления:
- safe_proxy_restart: удаление lock-файла после фонового перезапуска
  с защитой от stale lock (>300 сек)
- write_json_file: атомарная запись через tmp+mv вместо прямой
- write_reload_state: атомарная запись через tmp+mv
- state_add_list: отклонение значений с пробелами

Исправления безопасности:
- killall logread заменён на pkill -f 'logread -f'
- SIGTERM→SIGKILL для Telegram и sing-box при остановке
- Recover OOM scale: инкремент +0.05 каждые 10 минут
- nfqueue и byedpi: trap для корректного завершения дочерних процессов
- nfqueue: валидация стратегий и откат при ошибке запуска
- nfqueue: flush mangle_output перед добавлением правил
- Исправлен парсинг exit code в 14 файлах (status & 127)

Улучшения стабильности:
- init.d: trap для start_service/stop_service
- watchdog: signal traps для legacy fallback loop
- lifecycle: логирование ошибок при остановке компонентов
- singbox/priority: SIGTERM→SIGKILL при остановке
- telegram: exponential backoff (1x→300s) при ошибках API
- telegram: process_updates возвращает boolean для отслеживания успеха
- watchdog: кэш proxy port для уменьшения чтения конфига sing-box
- singbox/generator: упрощён atomic_write_json_file
- packages.uc: переупорядочивание apk_version (лёгкие запросы первыми)
- dns/apply: проверка PID sing-box перед настройкой dnsmasq
- server/service: cleanup_orphaned_server_certs перед вызовом
This commit is contained in:
Dushnilin
2026-08-06 00:03:51 +03:00
parent cb62d7e006
commit 488bbfefd5
25 changed files with 328 additions and 78 deletions
+2
View File
@@ -3,6 +3,7 @@ fe-app-tachyon/.yarn/
fe-app-tachyon/.env fe-app-tachyon/.env
.DS_Store .DS_Store
shellcheck-v0.10.0/ shellcheck-v0.10.0/
AGENTS.md
opencode.json opencode.json
sandbox/ sandbox/
@@ -12,3 +13,4 @@ build-out/
scratch/ scratch/
tmp_*/ tmp_*/
tmp_test*.uc tmp_test*.uc
test_out.txt
+4
View File
@@ -30,8 +30,10 @@ sync_service_triggers_payload() {
} }
start_service() { start_service() {
trap 'exit 1' INT TERM
initd_ucode start-service "$1" "$$" initd_ucode start-service "$1" "$$"
TACHYON_LAST_START_STATUS="$?" TACHYON_LAST_START_STATUS="$?"
trap - INT TERM
return "$TACHYON_LAST_START_STATUS" return "$TACHYON_LAST_START_STATUS"
} }
@@ -40,7 +42,9 @@ service_started() {
} }
stop_service() { stop_service() {
trap 'exit 1' INT TERM
initd_ucode stop-service "$$" initd_ucode stop-service "$$"
trap - INT TERM
} }
reload_service() { reload_service() {
+6 -1
View File
@@ -24,7 +24,12 @@ function command_from_args(args) {
function normalize_status(status) { function normalize_status(status) {
status = int(status); status = int(status);
return status > 255 ? int(status / 256) : status; if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
} }
function module_path(path) { function module_path(path) {
+6 -1
View File
@@ -55,7 +55,12 @@ function command_env(assignments) {
function command_status(command) { function command_status(command) {
let status = int(system(command)); let status = int(system(command));
return status > 255 ? int(status / 256) : status; if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
} }
function command_success(command) { function command_success(command) {
+6 -1
View File
@@ -151,7 +151,12 @@ function command_output_from_args(args) {
function command_status(command) { function command_status(command) {
let status = int(system(command)); let status = int(system(command));
return status > 255 ? int(status / 256) : status; if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
} }
function command_success(command) { function command_success(command) {
+28 -3
View File
@@ -64,7 +64,19 @@ function csv_to_json_array(value) {
} }
function write_json_file(path, value) { function write_json_file(path, value) {
return fs.writefile(path, sprintf("%J\n", value)); path = as_string(path);
let stamp = clock();
let tmp_path = sprintf("%s.%d.%d.tmp", path, stamp[0], stamp[1]);
let result = fs.writefile(tmp_path, sprintf("%J\n", value));
if (result == null || (type(result) == "boolean" && !result)) {
try { fs.unlink(tmp_path); } catch(e) {}
return false;
}
if (!fs.rename(tmp_path, path)) {
try { fs.unlink(tmp_path); } catch(e) {}
return false;
}
return true;
} }
function strip_internal_fields(value) { function strip_internal_fields(value) {
@@ -147,7 +159,12 @@ function command_from_args(args) {
function command_status(command) { function command_status(command) {
let status = int(system(command)); let status = int(system(command));
return status > 255 ? int(status / 256) : status; if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
} }
function command_success(command) { function command_success(command) {
@@ -163,7 +180,15 @@ function command_capture(command) {
if (!p) return null; if (!p) return null;
let output = as_string(p.read("all") || ""); let output = as_string(p.read("all") || "");
let status = p.close(); let status = p.close();
status = status > 255 ? int(status / 256) : status; if (status == -1)
status = 255;
else {
let signal = status & 127;
if (signal != 0)
status = 128 + signal;
else
status = (status >> 8) & 255;
}
return { status: status, output: output }; return { status: status, output: output };
} }
+5 -5
View File
@@ -139,15 +139,15 @@ function apk_version(package_name) {
if (version != "") if (version != "")
return version; return version;
version = apk_info_version(package_name, command_output([ "apk", "info", "-v", package_name ]));
if (version != "")
return version;
version = apk_manifest_version(package_name, command_output([ "apk", "list", "--installed", "--manifest", package_name ])); version = apk_manifest_version(package_name, command_output([ "apk", "list", "--installed", "--manifest", package_name ]));
if (version != "") if (version != "")
return version; return version;
version = apk_manifest_version(package_name, command_output([ "apk", "list", "--installed", "--manifest" ])); return apk_manifest_version(package_name, command_output([ "apk", "list", "--installed", "--manifest" ]));
if (version != "")
return version;
return apk_info_version(package_name, command_output([ "apk", "info", "-v", package_name ]));
} }
function apk_available_version(package_name) { function apk_available_version(package_name) {
+4 -1
View File
@@ -155,8 +155,11 @@ function state_set(path, value) {
} }
function state_add_list(path, value) { function state_add_list(path, value) {
value = as_string(value);
if (match(value, /[ \t\r\n]/) != null)
return false;
let current = state_get(path); let current = state_get(path);
return state_set(path, current == "" ? value : current + " " + as_string(value)); return state_set(path, current == "" ? value : current + " " + value);
} }
function state_del_list(path, value) { function state_del_list(path, value) {
+6 -1
View File
@@ -84,7 +84,12 @@ function command_from_args(args) {
function normalize_status(status) { function normalize_status(status) {
status = int(status); status = int(status);
return status > 255 ? int(status / 256) : status; if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
} }
function command_status(command) { function command_status(command) {
+8
View File
@@ -244,6 +244,14 @@ function dnsmasq_configure(force) {
log("Previous Tachyon shutdown was unclean and dnsmasq is not ready; applying Tachyon DNS settings", "info"); log("Previous Tachyon shutdown was unclean and dnsmasq is not ready; applying Tachyon DNS settings", "info");
} }
if (as_string(force) != "force") {
let sb_pid = trim(fs.readfile("/var/run/sing-box.pid") || "");
if (sb_pid == "" || !run("kill -0 " + shell_quote(sb_pid) + " 2>/dev/null")) {
log("sing-box is not running; dnsmasq will forward to system DNS until sing-box starts", "warn");
return true;
}
}
log("Configuring dnsmasq to forward DNS to sing-box", "info"); log("Configuring dnsmasq to forward DNS to sing-box", "info");
dnsmasq_cleanup_legacy_instance(); dnsmasq_cleanup_legacy_instance();
dnsmasq_configure_default_instance(); dnsmasq_configure_default_instance();
@@ -62,7 +62,12 @@ function command_output_from_args(args) {
function command_status(command) { function command_status(command) {
let status = int(system(command)); let status = int(system(command));
return status > 255 ? int(status / 256) : status; if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
} }
function command_success_from_args(args) { function command_success_from_args(args) {
@@ -290,8 +295,10 @@ function supervisor_command(port, raw_opt, child_pidfile) {
for (let word in strategy_words(raw_opt)) for (let word in strategy_words(raw_opt))
push(args, word); push(args, word);
return "ulimit -n " + shell_quote(BYEDPI_OPEN_FILES_LIMIT) + " >/dev/null 2>&1 || true; " + let child_pidfile_q = shell_quote(child_pidfile);
command_from_args(args) + " & child=$!; echo $child > " + shell_quote(child_pidfile) + "; wait $child; rc=$?; rm -f " + shell_quote(child_pidfile) + "; exit $rc"; return "trap 'kill $child 2>/dev/null; rm -f " + child_pidfile_q + "; exit 0' TERM INT; " +
"ulimit -n " + shell_quote(BYEDPI_OPEN_FILES_LIMIT) + " >/dev/null 2>&1 || true; " +
command_from_args(args) + " & child=$!; echo $child > " + child_pidfile_q + "; wait $child; rc=$?; rm -f " + child_pidfile_q + "; exit $rc";
} }
function supervisor(section, port, raw_opt, child_pidfile) { function supervisor(section, port, raw_opt, child_pidfile) {
@@ -315,7 +322,7 @@ function start_rule(section, index_value) {
let validation = validator().validate_byedpi_strategy(raw_opt); let validation = validator().validate_byedpi_strategy(raw_opt);
if (!validation.valid) { if (!validation.valid) {
log_message("Invalid ByeDPI strategy for rule '" + name + "': " + validation.message, "fatal"); log_message("Invalid ByeDPI strategy for rule '" + name + "': " + validation.message, "fatal");
exit(1); return false;
} }
let pidfile = BYEDPI_PID_DIR + "/" + name + ".pid"; let pidfile = BYEDPI_PID_DIR + "/" + name + ".pid";
@@ -335,19 +342,20 @@ function start_rule(section, index_value) {
]) + " >>" + shell_quote(logfile) + " 2>&1 1000>&- & echo $!"; ]) + " >>" + shell_quote(logfile) + " 2>&1 1000>&- & echo $!";
let pid = trim(command_output("sh -c " + shell_quote(command))); let pid = trim(command_output("sh -c " + shell_quote(command)));
if (pid == "" || !fs.writefile(pidfile, pid + "\n")) { if (pid == "" || !fs.writefile(pidfile, pid + "\n")) {
log_message("ciadpi failed to start for rule '" + name + "'. Check " + logfile + ". Aborted.", "fatal"); log_message("ciadpi failed to start for rule '" + name + "'. Check " + logfile + ".", "fatal");
exit(1); return false;
} }
command_success_from_args([ "sleep", "1" ]); command_success_from_args([ "sleep", "1" ]);
if (!runtime_pid_running(pid)) { if (!runtime_pid_running(pid)) {
log_message("ciadpi failed to start for rule '" + name + "'. Check " + logfile + ". Aborted.", "fatal"); log_message("ciadpi failed to start for rule '" + name + "'. Check " + logfile + ".", "fatal");
exit(1); return false;
} }
let child_pid = file_first_line(child_pidfile); let child_pid = file_first_line(child_pidfile);
if (child_pid == "" || !runtime_pid_running(child_pid)) if (child_pid == "" || !runtime_pid_running(child_pid))
log_message("ciadpi supervisor started for rule '" + name + "', but ciadpi is not running yet. Check " + logfile + ".", "warn"); log_message("ciadpi supervisor started for rule '" + name + "', but ciadpi is not running yet. Check " + logfile + ".", "warn");
return true;
} }
function start_runtime() { function start_runtime() {
@@ -375,9 +383,21 @@ function start_runtime() {
exit(1); exit(1);
} }
let started_sections = [];
let index_value = 1; let index_value = 1;
for (let section in sections) { for (let section in sections) {
start_rule(section, index_value); if (!start_rule(section, index_value)) {
log_message("Rolling back " + as_string(length(started_sections)) + " started ciadpi rules.", "warn");
for (let started in started_sections) {
let name = section_name(started);
let pidfile = BYEDPI_PID_DIR + "/" + name + ".pid";
let child_pidfile = BYEDPI_CHILD_PID_DIR + "/" + name + ".pid";
kill_pidfile_process(pidfile, "9");
kill_pidfile_process(child_pidfile, "9");
}
exit(1);
}
push(started_sections, section);
index_value++; index_value++;
} }
} }
@@ -52,7 +52,12 @@ function command_output_from_args(args) {
function command_status(command) { function command_status(command) {
let status = int(system(command)); let status = int(system(command));
return status > 255 ? int(status / 256) : status; if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
} }
function command_success(command) { function command_success(command) {
@@ -373,6 +378,13 @@ function validate_strategy_or_exit(cfg, section_name_value, raw_opt) {
exit(1); exit(1);
} }
function validate_strategy(cfg, section_name_value, raw_opt) {
let result = cfg.validator().validate_strategy(cfg.validator_kind, raw_opt, cfg.legacy_default_strategy);
if (!result.valid)
log_message("Invalid " + cfg.binary_name + " strategy for rule '" + section_name_value + "': " + result.message, "fatal");
return result.valid;
}
function supervisor_command(cfg, queue, raw_opt, child_pidfile) { function supervisor_command(cfg, queue, raw_opt, child_pidfile) {
let args = [ cfg.binary, "--qnum=" + as_string(queue) ]; let args = [ cfg.binary, "--qnum=" + as_string(queue) ];
for (let arg in base_args(cfg)) for (let arg in base_args(cfg))
@@ -380,7 +392,9 @@ function supervisor_command(cfg, queue, raw_opt, child_pidfile) {
for (let word in strategy_words(raw_opt)) for (let word in strategy_words(raw_opt))
push(args, word); push(args, word);
return command_from_args(args) + " & child=$!; echo $child > " + shell_quote(child_pidfile) + "; wait $child; rc=$?; rm -f " + shell_quote(child_pidfile) + "; exit $rc"; let child_pidfile_q = shell_quote(child_pidfile);
return "trap 'kill $child 2>/dev/null; rm -f " + child_pidfile_q + "; exit 0' TERM INT; " +
command_from_args(args) + " & child=$!; echo $child > " + child_pidfile_q + "; wait $child; rc=$?; rm -f " + child_pidfile_q + "; exit $rc";
} }
function supervisor(cfg, section, queue, raw_opt, child_pidfile) { function supervisor(cfg, section, queue, raw_opt, child_pidfile) {
@@ -423,7 +437,8 @@ function start_rule(cfg, section, index_value) {
let queue = queue_number(cfg, index_value); let queue = queue_number(cfg, index_value);
let mark = route_mark_hex(cfg, index_value); let mark = route_mark_hex(cfg, index_value);
let raw_opt = expand_strategy(cfg, raw_strategy(cfg, section)); let raw_opt = expand_strategy(cfg, raw_strategy(cfg, section));
validate_strategy_or_exit(cfg, name, raw_opt); if (!validate_strategy(cfg, name, raw_opt))
return false;
let pidfile = cfg.pid_dir + "/" + name + ".pid"; let pidfile = cfg.pid_dir + "/" + name + ".pid";
let child_pidfile = cfg.child_pid_dir + "/" + name + ".pid"; let child_pidfile = cfg.child_pid_dir + "/" + name + ".pid";
@@ -442,19 +457,20 @@ function start_rule(cfg, section, index_value) {
]) + " >>" + shell_quote(logfile) + " 2>&1 1000>&- & echo $!"; ]) + " >>" + shell_quote(logfile) + " 2>&1 1000>&- & echo $!";
let pid = trim(command_output("sh -c " + shell_quote(command))); let pid = trim(command_output("sh -c " + shell_quote(command)));
if (pid == "" || fs.writefile(pidfile, pid + "\n") == null) { if (pid == "" || fs.writefile(pidfile, pid + "\n") == null) {
log_message(cfg.binary_name + " failed to start for rule '" + name + "'. Check " + logfile + ". Aborted.", "fatal"); log_message(cfg.binary_name + " failed to start for rule '" + name + "'. Check " + logfile + ".", "fatal");
exit(1); return false;
} }
command_success_from_args([ "sleep", "1" ]); command_success_from_args([ "sleep", "1" ]);
if (!runtime_pid_running(pid)) { if (!runtime_pid_running(pid)) {
log_message(cfg.binary_name + " failed to start for rule '" + name + "'. Check " + logfile + ". Aborted.", "fatal"); log_message(cfg.binary_name + " failed to start for rule '" + name + "'. Check " + logfile + ".", "fatal");
exit(1); return false;
} }
let child_pid = file_first_line(child_pidfile); let child_pid = file_first_line(child_pidfile);
if (child_pid == "" || !runtime_pid_running(child_pid)) if (child_pid == "" || !runtime_pid_running(child_pid))
log_message(cfg.binary_name + " supervisor started for rule '" + name + "', but " + cfg.binary_name + " is not running yet. Check " + logfile + ".", "warn"); log_message(cfg.binary_name + " supervisor started for rule '" + name + "', but " + cfg.binary_name + " is not running yet. Check " + logfile + ".", "warn");
return true;
} }
function start_runtime(cfg) { function start_runtime(cfg) {
@@ -470,9 +486,21 @@ function start_runtime(cfg) {
exit(1); exit(1);
} }
let started_sections = [];
let index_value = 1; let index_value = 1;
for (let section in sections) { for (let section in sections) {
start_rule(cfg, section, index_value); if (!start_rule(cfg, section, index_value)) {
log_message("Rolling back " + as_string(length(started_sections)) + " started " + cfg.binary_name + " rules.", "warn");
for (let started in started_sections) {
let name = section_name(started);
let pidfile = cfg.pid_dir + "/" + name + ".pid";
let child_pidfile = cfg.child_pid_dir + "/" + name + ".pid";
kill_pidfile_process(pidfile, "9");
kill_pidfile_process(child_pidfile, "9");
}
exit(1);
}
push(started_sections, section);
index_value++; index_value++;
} }
} }
@@ -655,6 +683,8 @@ function create_nft_rules(cfg) {
if (length(sections) == 0 || !provider_available(cfg)) if (length(sections) == 0 || !provider_available(cfg))
return; return;
command_success_from_args([ "nft", "flush", "chain", "inet", NFT_TABLE_NAME, "mangle_output" ]);
command_success_from_args([ "nft", "add", "rule", "inet", NFT_TABLE_NAME, "mangle_output", "meta", "mark", "&", cfg.desync_mark, "==", cfg.desync_mark, "return" ]); command_success_from_args([ "nft", "add", "rule", "inet", NFT_TABLE_NAME, "mangle_output", "meta", "mark", "&", cfg.desync_mark, "==", cfg.desync_mark, "return" ]);
command_success_from_args([ "nft", "add", "rule", "inet", NFT_TABLE_NAME, "mangle_output", "meta", "mark", "&", cfg.desync_mark_postnat, "==", cfg.desync_mark_postnat, "return" ]); command_success_from_args([ "nft", "add", "rule", "inet", NFT_TABLE_NAME, "mangle_output", "meta", "mark", "&", cfg.desync_mark_postnat, "==", cfg.desync_mark_postnat, "return" ]);
+21
View File
@@ -996,10 +996,31 @@ function prepare_server_defaults(section) {
server_prepare_tls_defaults(section, protocol, security); server_prepare_tls_defaults(section, protocol, security);
} }
function cleanup_orphaned_server_certs() {
let cert_dir = "/etc/tachyon/server-certs";
try {
let entries = fs.readdir(cert_dir);
if (entries == null) return;
let active_names = {};
for (let section in uci_sections("server")) {
let name = safe_filename_string(section);
active_names[name + ".crt"] = true;
active_names[name + ".key"] = true;
}
for (let entry in entries) {
if (substr(entry, 0, 1) == ".") continue;
if (active_names[entry]) continue;
let path = cert_dir + "/" + entry;
try { fs.unlink(path); } catch(e) {}
}
} catch(e) {}
}
function prepare_all_server_defaults() { function prepare_all_server_defaults() {
SERVER_DEFAULTS_CHANGED = false; SERVER_DEFAULTS_CHANGED = false;
for (let section in uci_sections("server")) for (let section in uci_sections("server"))
prepare_server_defaults(section); prepare_server_defaults(section);
cleanup_orphaned_server_certs();
if (SERVER_DEFAULTS_CHANGED) if (SERVER_DEFAULTS_CHANGED)
uci_commit(CONFIG_NAME); uci_commit(CONFIG_NAME);
} }
+8 -2
View File
@@ -49,7 +49,12 @@ function command_from_args(args) {
function normalize_status(status) { function normalize_status(status) {
status = int(status); status = int(status);
return status > 255 ? int(status / 256) : status; if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
} }
function command_status(command) { function command_status(command) {
@@ -576,7 +581,8 @@ function stop_service(owner_pid) {
// Clean up orphaned logread -f processes left by watchdog instances. // Clean up orphaned logread -f processes left by watchdog instances.
// These survive tachyon stop because background subprocesses (reload_firewall, // These survive tachyon stop because background subprocesses (reload_firewall,
// restart) inherit the logread pipe read-end, preventing SIGPIPE delivery. // restart) inherit the logread pipe read-end, preventing SIGPIPE delivery.
system("killall logread 2>/dev/null; true"); // Use pkill to target only logread in follow mode, not one-shot logread calls.
system("pkill -f 'logread -f' 2>/dev/null; true");
return stop_finish(job_id, status); return stop_finish(job_id, status);
} }
+16 -6
View File
@@ -136,7 +136,12 @@ function command_from_args(args) {
function normalize_status(status) { function normalize_status(status) {
status = int(status); status = int(status);
return status > 255 ? int(status / 256) : status; if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
} }
function command_status(command) { function command_status(command) {
@@ -852,16 +857,21 @@ function stop_main() {
let status = 0; let status = 0;
log_message("Stopping Tachyon", "info"); log_message("Stopping Tachyon", "info");
module_success(DNS_FAILOVER_UC, [ "stop-runtime" ]); if (!module_success(DNS_FAILOVER_UC, [ "stop-runtime" ]))
module_success(PRIORITY_UC, [ "stop-runtime" ]); log_message("DNS failover stop failed (non-fatal)", "warn");
if (!module_success(PRIORITY_UC, [ "stop-runtime" ]))
log_message("Priority daemon stop failed (non-fatal)", "warn");
module_success(SUBSCRIPTION_CACHE_UC, [ "stop-deferred-bootstrap-worker" ]); module_success(SUBSCRIPTION_CACHE_UC, [ "stop-deferred-bootstrap-worker" ]);
module_success(UPDATES_UC, [ "stop-list-update" ]); module_success(UPDATES_UC, [ "stop-list-update" ]);
remove_cron_jobs(); remove_cron_jobs();
command_success_from_args([ "find", TMP_RULESET_FOLDER, "-mindepth", "1", "-maxdepth", "1", "-type", "f", "-delete" ]); command_success_from_args([ "find", TMP_RULESET_FOLDER, "-mindepth", "1", "-maxdepth", "1", "-type", "f", "-delete" ]);
module_success(ZAPRET_UC, [ "stop-runtime" ]); if (!module_success(ZAPRET_UC, [ "stop-runtime" ]))
module_success(ZAPRET2_UC, [ "stop-runtime" ]); log_message("Zapret stop failed (non-fatal)", "warn");
module_success(BYEDPI_UC, [ "stop-runtime" ]); if (!module_success(ZAPRET2_UC, [ "stop-runtime" ]))
log_message("Zapret2 stop failed (non-fatal)", "warn");
if (!module_success(BYEDPI_UC, [ "stop-runtime" ]))
log_message("ByeDPI stop failed (non-fatal)", "warn");
if (command_success_from_args([ "nft", "list", "table", "inet", NFT_TABLE_NAME ])) { if (command_success_from_args([ "nft", "list", "table", "inet", NFT_TABLE_NAME ])) {
if (!command_success_from_args([ "nft", "delete", "table", "inet", NFT_TABLE_NAME ])) if (!command_success_from_args([ "nft", "delete", "table", "inet", NFT_TABLE_NAME ]))
+6 -1
View File
@@ -37,7 +37,12 @@ function command_from_args(args) {
function normalize_status(status) { function normalize_status(status) {
status = int(status); status = int(status);
return status > 255 ? int(status / 256) : status; if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
} }
function command_success_from_args(args) { function command_success_from_args(args) {
+11 -1
View File
@@ -337,8 +337,18 @@ function write_reload_state(path, values) {
if (!ensure_parent_dir(path)) if (!ensure_parent_dir(path))
exit(1); exit(1);
if (!fs.writefile(path, reload_state_text(values))) path = as_string(path);
let stamp = clock();
let tmp_path = sprintf("%s.%d.%d.tmp", path, stamp[0], stamp[1]);
let result = fs.writefile(tmp_path, reload_state_text(values));
if (result == null || (type(result) == "boolean" && !result)) {
try { fs.unlink(tmp_path); } catch(e) {}
exit(1); exit(1);
}
if (!fs.rename(tmp_path, path)) {
try { fs.unlink(tmp_path); } catch(e) {}
exit(1);
}
} }
function copy_file(source, target) { function copy_file(source, target) {
+26 -5
View File
@@ -2095,7 +2095,7 @@ function process_updates(token, admin_ids) {
let offset = int(trim(fs.readfile(OFFSET_FILE) || "0")); let offset = int(trim(fs.readfile(OFFSET_FILE) || "0"));
let res = tg_request(token, "getUpdates", { offset: offset, timeout: 20 }); let res = tg_request(token, "getUpdates", { offset: offset, timeout: 20 });
if (!res || !res.ok || !res.result || length(res.result) == 0) return; if (!res || !res.ok || !res.result || length(res.result) == 0) return false;
for (let upd in res.result) { for (let upd in res.result) {
let update_id = upd.update_id; let update_id = upd.update_id;
@@ -2292,6 +2292,7 @@ function process_updates(token, admin_ids) {
} }
} }
fs.writefile(OFFSET_FILE, as_string(offset)); fs.writefile(OFFSET_FILE, as_string(offset));
return true;
} }
// ─── Entry Point ───────────────────────────────────────────────────────────── // ─── Entry Point ─────────────────────────────────────────────────────────────
@@ -2386,15 +2387,26 @@ function worker() {
let last_report_day = -1; let last_report_day = -1;
let last_update_check = 0; let last_update_check = 0;
let consecutive_failures = 0;
while (true) { while (true) {
try { try {
cfg = settings(); cfg = settings();
if (cfg.enabled != "1") break; if (cfg.enabled != "1") break;
process_updates(cfg.bot_token, cfg.admin_ids); let res = process_updates(cfg.bot_token, cfg.admin_ids);
if (res === false) {
consecutive_failures++;
let backoff = poll_interval * (1 << min(consecutive_failures - 1, 4));
if (backoff > 300) backoff = 300;
command_success_from_args(["logger", "-t", "tachyon-telegram", "[warn] API failure " + as_string(consecutive_failures) + ", backing off " + as_string(backoff) + "s"]);
sleep(backoff * 1000);
continue;
}
consecutive_failures = 0;
let now = time(); let now = time();
let tm = clock(now); // [year, mon, day, hour, min, sec] let tm = clock(now);
let daily_hour = int(cfg.daily_report_hour || "8"); let daily_hour = int(cfg.daily_report_hour || "8");
if (cfg.daily_report_enabled == "1" && tm[3] == daily_hour && tm[2] != last_report_day) { if (cfg.daily_report_enabled == "1" && tm[3] == daily_hour && tm[2] != last_report_day) {
@@ -2402,11 +2414,12 @@ function worker() {
send_daily_digest(cfg.bot_token, cfg.admin_ids); send_daily_digest(cfg.bot_token, cfg.admin_ids);
} }
if (now - last_update_check > 3600) { // check every hour if (now - last_update_check > 3600) {
check_notified_updates(cfg.bot_token, cfg.admin_ids); check_notified_updates(cfg.bot_token, cfg.admin_ids);
last_update_check = now; last_update_check = now;
} }
} catch (e) { } catch (e) {
consecutive_failures++;
command_success_from_args(["logger", "-t", "tachyon-telegram", "[err] Worker loop error: " + as_string(e)]); command_success_from_args(["logger", "-t", "tachyon-telegram", "[err] Worker loop error: " + as_string(e)]);
} }
@@ -2418,9 +2431,17 @@ function worker() {
function stop_runtime() { function stop_runtime() {
let pid = trim(fs.readfile(PID_FILE) || ""); let pid = trim(fs.readfile(PID_FILE) || "");
if (pid != "" && match(pid, /^[0-9]+$/) != null && command_success_from_args([ "kill", "-0", pid ])) { if (pid != "" && match(pid, /^[0-9]+$/) != null && command_success_from_args([ "kill", "-0", pid ])) {
command_success_from_args([ "kill", pid ]);
let wait_limit = 30;
while (wait_limit > 0 && command_success_from_args([ "kill", "-0", pid ])) {
sleep(100);
wait_limit--;
}
if (command_success_from_args([ "kill", "-0", pid ])) {
command_success_from_args([ "kill", "-9", pid ]); command_success_from_args([ "kill", "-9", pid ]);
} }
fs.unlink(PID_FILE); }
try { fs.unlink(PID_FILE); } catch(e) {}
return 0; return 0;
} }
+6 -1
View File
@@ -119,7 +119,12 @@ function command_output_from_args(args) {
function command_status(command) { function command_status(command) {
let status = int(system(command)); let status = int(system(command));
return status > 255 ? int(status / 256) : status; if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
} }
function command_success(command) { function command_success(command) {
+45 -4
View File
@@ -146,6 +146,7 @@ function stop_runtime() {
} }
remove_file("/var/run/tachyon_honeypot_listener.pid"); remove_file("/var/run/tachyon_honeypot_listener.pid");
remove_file("/tmp/tachyon_honeypot.fifo"); remove_file("/tmp/tachyon_honeypot.fifo");
remove_file(PROXY_RESTART_LOCK);
return 0; return 0;
} }
@@ -279,6 +280,7 @@ try { uloop = require("uloop"); } catch (e) {}
try { ubus = require("ubus"); } catch (e) {} try { ubus = require("ubus"); } catch (e) {}
let last_oom_time = 0; let last_oom_time = 0;
let last_oom_recovery_time = 0;
let last_restart_time = 0; let last_restart_time = 0;
let last_urltest_check = 0; let last_urltest_check = 0;
let pending_smart_domains = {}; let pending_smart_domains = {};
@@ -290,6 +292,7 @@ let last_reload_time = 0;
let proxy_consecutive_fails = 0; let proxy_consecutive_fails = 0;
let dns_consecutive_fails = 0; let dns_consecutive_fails = 0;
let ai_healthy_streak = 0; let ai_healthy_streak = 0;
let cached_proxy_port = null;
let proxy_latency_history = []; let proxy_latency_history = [];
let dns_latency_history = []; let dns_latency_history = [];
let last_anomaly_check = 0; let last_anomaly_check = 0;
@@ -465,12 +468,20 @@ function safe_proxy_restart(reason) {
return false; return false;
} }
if (fs.stat(PROXY_RESTART_LOCK) != null) { if (fs.stat(PROXY_RESTART_LOCK) != null) {
log_message("Proxy restart lock exists, skipping (" + reason + ")", "warn"); let lock_content = trim(fs.readfile(PROXY_RESTART_LOCK) || "0");
let lock_age = now - int(lock_content);
if (lock_age < 300) {
log_message("Proxy restart lock exists (age " + as_string(lock_age) + "s), skipping (" + reason + ")", "warn");
return false; return false;
} }
log_message("Proxy restart lock stale (age " + as_string(lock_age) + "s), removing", "warn");
remove_file(PROXY_RESTART_LOCK);
}
try { fs.writefile(PROXY_RESTART_LOCK, as_string(now)); } catch(e) {} try { fs.writefile(PROXY_RESTART_LOCK, as_string(now)); } catch(e) {}
proxy_restart_count++; proxy_restart_count++;
bg_system("/etc/init.d/tachyon restart </dev/null >/dev/null 2>&1 &"); cached_proxy_port = null;
let lock_path = shell_quote(PROXY_RESTART_LOCK);
bg_system("/etc/init.d/tachyon restart </dev/null >/dev/null 2>&1 & rm -f " + lock_path + " &");
return true; return true;
} }
@@ -562,6 +573,9 @@ function ai_heal_proxy_connectivity() {
let now = time(); let now = time();
let proxy_addr = "127.0.0.1:4534"; let proxy_addr = "127.0.0.1:4534";
if (cached_proxy_port !== null) {
proxy_addr = "127.0.0.1:" + as_string(cached_proxy_port);
} else {
let sb_cfg_data = fs.readfile("/etc/sing-box/config.json"); let sb_cfg_data = fs.readfile("/etc/sing-box/config.json");
if (sb_cfg_data) { if (sb_cfg_data) {
try { try {
@@ -569,13 +583,15 @@ function ai_heal_proxy_connectivity() {
if (sb_cfg.inbounds) { if (sb_cfg.inbounds) {
for (let inb in sb_cfg.inbounds) { for (let inb in sb_cfg.inbounds) {
if (inb.type == "http" || inb.type == "mixed") { if (inb.type == "http" || inb.type == "mixed") {
proxy_addr = "127.0.0.1:" + as_string(inb.listen_port || 4534); cached_proxy_port = inb.listen_port || 4534;
proxy_addr = "127.0.0.1:" + as_string(cached_proxy_port);
break; break;
} }
} }
} }
} catch(e) {} } catch(e) {}
} }
}
let proxy_ok = command_success_from_args([ let proxy_ok = command_success_from_args([
"curl", "-s", "-I", "--connect-timeout", "3", "--max-time", "5", "curl", "-s", "-I", "--connect-timeout", "3", "--max-time", "5",
@@ -1497,6 +1513,27 @@ function setup_honeypot_listener() {
} }
} }
function recover_oom_scale() {
let now = time();
if (now - last_oom_time < 1800) return;
if (last_oom_time == 0) return;
if (now - last_oom_recovery_time < 600) return;
last_oom_recovery_time = now;
let scale_path = "/etc/tachyon/mem_scale";
let scale_data = fs.readfile(scale_path);
if (scale_data == null) return;
let current_scale = double(trim(as_string(scale_data)));
if (current_scale >= 1.0 || current_scale < 0.1) {
if (current_scale >= 1.0) try { fs.unlink(scale_path); } catch(e) {}
return;
}
let new_scale = current_scale + 0.05;
if (new_scale > 1.0) new_scale = 1.0;
log_message("OOM recovery: restoring GOMEMLIMIT scale from " + sprintf("%.2f", current_scale) + " to " + sprintf("%.2f", new_scale), "info");
try { fs.writefile(scale_path, sprintf("%.2f", new_scale)); } catch(e) {}
}
function setup_syslog_listener() { function setup_syslog_listener() {
if (!uloop) return null; if (!uloop) return null;
syslog_start_time = time(); syslog_start_time = time();
@@ -1504,7 +1541,8 @@ function setup_syslog_listener() {
// Without this, every restart cascades: new watchdog inherits old watchdog's // Without this, every restart cascades: new watchdog inherits old watchdog's
// logread pipe read-end, keeping old logread alive. Over N restarts, // logread pipe read-end, keeping old logread alive. Over N restarts,
// watchdog accumulates N inherited FDs → hits 1024 limit → config generator fails. // watchdog accumulates N inherited FDs → hits 1024 limit → config generator fails.
system("killall logread 2>/dev/null; true"); // Use pkill to target only logread in follow mode, not one-shot logread calls.
system("pkill -f 'logread -f' 2>/dev/null; true");
let log_pipe = fs.popen("logread -f 2>/dev/null", "r"); let log_pipe = fs.popen("logread -f 2>/dev/null", "r");
if (!log_pipe) return null; if (!log_pipe) return null;
// Track the FD so bg_system() can close it before spawning background processes // Track the FD so bg_system() can close it before spawning background processes
@@ -1601,6 +1639,7 @@ function worker() {
safe_call(ai_heal_empty_sections, "ai_heal_empty_sections"); safe_call(ai_heal_empty_sections, "ai_heal_empty_sections");
safe_call(ai_heal_dns_loop, "ai_heal_dns_loop"); safe_call(ai_heal_dns_loop, "ai_heal_dns_loop");
safe_call(analyze_anomalies, "analyze_anomalies"); safe_call(analyze_anomalies, "analyze_anomalies");
safe_call(recover_oom_scale, "recover_oom_scale");
} }
if (uloop) { if (uloop) {
@@ -1632,6 +1671,8 @@ function worker() {
uloop.run(); uloop.run();
} else { } else {
log_message("uloop not available. Running Watchdog in legacy fallback loop mode.", "warn"); log_message("uloop not available. Running Watchdog in legacy fallback loop mode.", "warn");
signal("SIGTERM", function(sig) { log_message("SIGTERM received, shutting down", "info"); stop_runtime(); exit(0); });
signal("SIGINT", function(sig) { log_message("SIGINT received, shutting down", "info"); stop_runtime(); exit(0); });
while (true) { while (true) {
perform_fast_checks(); perform_fast_checks();
perform_normal_checks(); perform_normal_checks();
@@ -32,7 +32,12 @@ function command_from_args(args) {
function command_status(command) { function command_status(command) {
let status = int(system(command)); let status = int(system(command));
return status > 255 ? int(status / 256) : status; if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
} }
function command_success_from_args(args) { function command_success_from_args(args) {
+1 -10
View File
@@ -121,18 +121,9 @@ function ensure_parent_dir(path) {
} }
function atomic_write_json_file(path, value) { function atomic_write_json_file(path, value) {
let stamp = clock();
let tmp_path = sprintf("%s.%d.%d.tmp", path, stamp[0], stamp[1]);
if (!ensure_parent_dir(path)) if (!ensure_parent_dir(path))
return false; return false;
if (!write_json_file(tmp_path, value)) return write_json_file(path, value);
return false;
if (!fs.rename(tmp_path, path)) {
fs.unlink(tmp_path);
return false;
}
return true;
} }
function fixture_section_list(type_name) { function fixture_section_list(type_name) {
+15 -2
View File
@@ -45,7 +45,12 @@ function command_output_from_args(args) {
function command_status(command) { function command_status(command) {
let status = int(system(command)); let status = int(system(command));
return status > 255 ? int(status / 256) : status; if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
} }
function command_success_from_args(args) { function command_success_from_args(args) {
@@ -436,8 +441,16 @@ function process_running(pid) {
function stop_runtime() { function stop_runtime() {
let pid = file_first_line(PRIORITY_PID_FILE); let pid = file_first_line(PRIORITY_PID_FILE);
if (process_running(pid)) if (process_running(pid)) {
command_success_from_args([ "kill", pid ]); command_success_from_args([ "kill", pid ]);
let wait_limit = 30;
while (wait_limit > 0 && process_running(pid)) {
sleep(100);
wait_limit--;
}
if (process_running(pid))
command_success_from_args([ "kill", "-9", pid ]);
}
remove_file(PRIORITY_PID_FILE); remove_file(PRIORITY_PID_FILE);
return 0; return 0;
} }
+6 -1
View File
@@ -79,7 +79,12 @@ function command_output_from_args(args) {
function command_status(command) { function command_status(command) {
let status = int(system(command)); let status = int(system(command));
return status > 255 ? int(status / 256) : status; if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
} }
function command_success_from_args(args) { function command_success_from_args(args) {
+6 -1
View File
@@ -603,7 +603,12 @@ function command_success_from_args(args) {
function command_status_from_args(args) { function command_status_from_args(args) {
let status = int(system(command_from_args(args))); let status = int(system(command_from_args(args)));
return status > 255 ? int(status / 256) : status; if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
} }
function run_silent(command) { function run_silent(command) {