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:
@@ -3,6 +3,7 @@ fe-app-tachyon/.yarn/
|
||||
fe-app-tachyon/.env
|
||||
.DS_Store
|
||||
shellcheck-v0.10.0/
|
||||
AGENTS.md
|
||||
opencode.json
|
||||
|
||||
sandbox/
|
||||
@@ -12,3 +13,4 @@ build-out/
|
||||
scratch/
|
||||
tmp_*/
|
||||
tmp_test*.uc
|
||||
test_out.txt
|
||||
@@ -30,8 +30,10 @@ sync_service_triggers_payload() {
|
||||
}
|
||||
|
||||
start_service() {
|
||||
trap 'exit 1' INT TERM
|
||||
initd_ucode start-service "$1" "$$"
|
||||
TACHYON_LAST_START_STATUS="$?"
|
||||
trap - INT TERM
|
||||
return "$TACHYON_LAST_START_STATUS"
|
||||
}
|
||||
|
||||
@@ -40,7 +42,9 @@ service_started() {
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
trap 'exit 1' INT TERM
|
||||
initd_ucode stop-service "$$"
|
||||
trap - INT TERM
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
|
||||
@@ -24,7 +24,12 @@ function command_from_args(args) {
|
||||
|
||||
function normalize_status(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) {
|
||||
|
||||
@@ -55,7 +55,12 @@ function command_env(assignments) {
|
||||
|
||||
function command_status(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) {
|
||||
|
||||
@@ -151,7 +151,12 @@ function command_output_from_args(args) {
|
||||
|
||||
function command_status(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) {
|
||||
|
||||
@@ -64,7 +64,19 @@ function csv_to_json_array(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) {
|
||||
@@ -147,7 +159,12 @@ function command_from_args(args) {
|
||||
|
||||
function command_status(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) {
|
||||
@@ -163,7 +180,15 @@ function command_capture(command) {
|
||||
if (!p) return null;
|
||||
let output = as_string(p.read("all") || "");
|
||||
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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -139,15 +139,15 @@ function apk_version(package_name) {
|
||||
if (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 ]));
|
||||
if (version != "")
|
||||
return version;
|
||||
|
||||
version = 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 ]));
|
||||
return apk_manifest_version(package_name, command_output([ "apk", "list", "--installed", "--manifest" ]));
|
||||
}
|
||||
|
||||
function apk_available_version(package_name) {
|
||||
|
||||
@@ -155,8 +155,11 @@ function state_set(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);
|
||||
return state_set(path, current == "" ? value : current + " " + as_string(value));
|
||||
return state_set(path, current == "" ? value : current + " " + value);
|
||||
}
|
||||
|
||||
function state_del_list(path, value) {
|
||||
|
||||
@@ -84,7 +84,12 @@ function command_from_args(args) {
|
||||
|
||||
function normalize_status(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) {
|
||||
|
||||
@@ -244,6 +244,14 @@ function dnsmasq_configure(force) {
|
||||
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");
|
||||
dnsmasq_cleanup_legacy_instance();
|
||||
dnsmasq_configure_default_instance();
|
||||
|
||||
@@ -62,7 +62,12 @@ function command_output_from_args(args) {
|
||||
|
||||
function command_status(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) {
|
||||
@@ -290,8 +295,10 @@ function supervisor_command(port, raw_opt, child_pidfile) {
|
||||
for (let word in strategy_words(raw_opt))
|
||||
push(args, word);
|
||||
|
||||
return "ulimit -n " + shell_quote(BYEDPI_OPEN_FILES_LIMIT) + " >/dev/null 2>&1 || true; " +
|
||||
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; " +
|
||||
"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) {
|
||||
@@ -315,7 +322,7 @@ function start_rule(section, index_value) {
|
||||
let validation = validator().validate_byedpi_strategy(raw_opt);
|
||||
if (!validation.valid) {
|
||||
log_message("Invalid ByeDPI strategy for rule '" + name + "': " + validation.message, "fatal");
|
||||
exit(1);
|
||||
return false;
|
||||
}
|
||||
|
||||
let pidfile = BYEDPI_PID_DIR + "/" + name + ".pid";
|
||||
@@ -335,19 +342,20 @@ function start_rule(section, index_value) {
|
||||
]) + " >>" + shell_quote(logfile) + " 2>&1 1000>&- & echo $!";
|
||||
let pid = trim(command_output("sh -c " + shell_quote(command)));
|
||||
if (pid == "" || !fs.writefile(pidfile, pid + "\n")) {
|
||||
log_message("ciadpi failed to start for rule '" + name + "'. Check " + logfile + ". Aborted.", "fatal");
|
||||
exit(1);
|
||||
log_message("ciadpi failed to start for rule '" + name + "'. Check " + logfile + ".", "fatal");
|
||||
return false;
|
||||
}
|
||||
|
||||
command_success_from_args([ "sleep", "1" ]);
|
||||
if (!runtime_pid_running(pid)) {
|
||||
log_message("ciadpi failed to start for rule '" + name + "'. Check " + logfile + ". Aborted.", "fatal");
|
||||
exit(1);
|
||||
log_message("ciadpi failed to start for rule '" + name + "'. Check " + logfile + ".", "fatal");
|
||||
return false;
|
||||
}
|
||||
|
||||
let child_pid = file_first_line(child_pidfile);
|
||||
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");
|
||||
return true;
|
||||
}
|
||||
|
||||
function start_runtime() {
|
||||
@@ -375,9 +383,21 @@ function start_runtime() {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let started_sections = [];
|
||||
let index_value = 1;
|
||||
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++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,12 @@ function command_output_from_args(args) {
|
||||
|
||||
function command_status(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) {
|
||||
@@ -373,6 +378,13 @@ function validate_strategy_or_exit(cfg, section_name_value, raw_opt) {
|
||||
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) {
|
||||
let args = [ cfg.binary, "--qnum=" + as_string(queue) ];
|
||||
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))
|
||||
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) {
|
||||
@@ -423,7 +437,8 @@ function start_rule(cfg, section, index_value) {
|
||||
let queue = queue_number(cfg, index_value);
|
||||
let mark = route_mark_hex(cfg, index_value);
|
||||
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 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 $!";
|
||||
let pid = trim(command_output("sh -c " + shell_quote(command)));
|
||||
if (pid == "" || fs.writefile(pidfile, pid + "\n") == null) {
|
||||
log_message(cfg.binary_name + " failed to start for rule '" + name + "'. Check " + logfile + ". Aborted.", "fatal");
|
||||
exit(1);
|
||||
log_message(cfg.binary_name + " failed to start for rule '" + name + "'. Check " + logfile + ".", "fatal");
|
||||
return false;
|
||||
}
|
||||
|
||||
command_success_from_args([ "sleep", "1" ]);
|
||||
if (!runtime_pid_running(pid)) {
|
||||
log_message(cfg.binary_name + " failed to start for rule '" + name + "'. Check " + logfile + ". Aborted.", "fatal");
|
||||
exit(1);
|
||||
log_message(cfg.binary_name + " failed to start for rule '" + name + "'. Check " + logfile + ".", "fatal");
|
||||
return false;
|
||||
}
|
||||
|
||||
let child_pid = file_first_line(child_pidfile);
|
||||
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");
|
||||
return true;
|
||||
}
|
||||
|
||||
function start_runtime(cfg) {
|
||||
@@ -470,9 +486,21 @@ function start_runtime(cfg) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let started_sections = [];
|
||||
let index_value = 1;
|
||||
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++;
|
||||
}
|
||||
}
|
||||
@@ -655,6 +683,8 @@ function create_nft_rules(cfg) {
|
||||
if (length(sections) == 0 || !provider_available(cfg))
|
||||
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_postnat, "==", cfg.desync_mark_postnat, "return" ]);
|
||||
|
||||
|
||||
@@ -996,10 +996,31 @@ function prepare_server_defaults(section) {
|
||||
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() {
|
||||
SERVER_DEFAULTS_CHANGED = false;
|
||||
for (let section in uci_sections("server"))
|
||||
prepare_server_defaults(section);
|
||||
cleanup_orphaned_server_certs();
|
||||
if (SERVER_DEFAULTS_CHANGED)
|
||||
uci_commit(CONFIG_NAME);
|
||||
}
|
||||
|
||||
@@ -49,7 +49,12 @@ function command_from_args(args) {
|
||||
|
||||
function normalize_status(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) {
|
||||
@@ -576,7 +581,8 @@ function stop_service(owner_pid) {
|
||||
// Clean up orphaned logread -f processes left by watchdog instances.
|
||||
// These survive tachyon stop because background subprocesses (reload_firewall,
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +136,12 @@ function command_from_args(args) {
|
||||
|
||||
function normalize_status(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) {
|
||||
@@ -852,16 +857,21 @@ function stop_main() {
|
||||
let status = 0;
|
||||
|
||||
log_message("Stopping Tachyon", "info");
|
||||
module_success(DNS_FAILOVER_UC, [ "stop-runtime" ]);
|
||||
module_success(PRIORITY_UC, [ "stop-runtime" ]);
|
||||
if (!module_success(DNS_FAILOVER_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(UPDATES_UC, [ "stop-list-update" ]);
|
||||
remove_cron_jobs();
|
||||
command_success_from_args([ "find", TMP_RULESET_FOLDER, "-mindepth", "1", "-maxdepth", "1", "-type", "f", "-delete" ]);
|
||||
|
||||
module_success(ZAPRET_UC, [ "stop-runtime" ]);
|
||||
module_success(ZAPRET2_UC, [ "stop-runtime" ]);
|
||||
module_success(BYEDPI_UC, [ "stop-runtime" ]);
|
||||
if (!module_success(ZAPRET_UC, [ "stop-runtime" ]))
|
||||
log_message("Zapret stop failed (non-fatal)", "warn");
|
||||
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", "delete", "table", "inet", NFT_TABLE_NAME ]))
|
||||
|
||||
@@ -37,7 +37,12 @@ function command_from_args(args) {
|
||||
|
||||
function normalize_status(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) {
|
||||
|
||||
@@ -337,9 +337,19 @@ function write_reload_state(path, values) {
|
||||
if (!ensure_parent_dir(path))
|
||||
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);
|
||||
}
|
||||
if (!fs.rename(tmp_path, path)) {
|
||||
try { fs.unlink(tmp_path); } catch(e) {}
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function copy_file(source, target) {
|
||||
let data = fs.readfile(source);
|
||||
|
||||
@@ -2095,7 +2095,7 @@ function process_updates(token, admin_ids) {
|
||||
let offset = int(trim(fs.readfile(OFFSET_FILE) || "0"));
|
||||
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) {
|
||||
let update_id = upd.update_id;
|
||||
@@ -2292,6 +2292,7 @@ function process_updates(token, admin_ids) {
|
||||
}
|
||||
}
|
||||
fs.writefile(OFFSET_FILE, as_string(offset));
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─── Entry Point ─────────────────────────────────────────────────────────────
|
||||
@@ -2386,15 +2387,26 @@ function worker() {
|
||||
|
||||
let last_report_day = -1;
|
||||
let last_update_check = 0;
|
||||
let consecutive_failures = 0;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
cfg = settings();
|
||||
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 tm = clock(now); // [year, mon, day, hour, min, sec]
|
||||
let tm = clock(now);
|
||||
let daily_hour = int(cfg.daily_report_hour || "8");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (now - last_update_check > 3600) { // check every hour
|
||||
if (now - last_update_check > 3600) {
|
||||
check_notified_updates(cfg.bot_token, cfg.admin_ids);
|
||||
last_update_check = now;
|
||||
}
|
||||
} catch (e) {
|
||||
consecutive_failures++;
|
||||
command_success_from_args(["logger", "-t", "tachyon-telegram", "[err] Worker loop error: " + as_string(e)]);
|
||||
}
|
||||
|
||||
@@ -2418,9 +2431,17 @@ function worker() {
|
||||
function stop_runtime() {
|
||||
let pid = trim(fs.readfile(PID_FILE) || "");
|
||||
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 ]);
|
||||
}
|
||||
fs.unlink(PID_FILE);
|
||||
}
|
||||
try { fs.unlink(PID_FILE); } catch(e) {}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +119,12 @@ function command_output_from_args(args) {
|
||||
|
||||
function command_status(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) {
|
||||
|
||||
@@ -146,6 +146,7 @@ function stop_runtime() {
|
||||
}
|
||||
remove_file("/var/run/tachyon_honeypot_listener.pid");
|
||||
remove_file("/tmp/tachyon_honeypot.fifo");
|
||||
remove_file(PROXY_RESTART_LOCK);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -279,6 +280,7 @@ try { uloop = require("uloop"); } catch (e) {}
|
||||
try { ubus = require("ubus"); } catch (e) {}
|
||||
|
||||
let last_oom_time = 0;
|
||||
let last_oom_recovery_time = 0;
|
||||
let last_restart_time = 0;
|
||||
let last_urltest_check = 0;
|
||||
let pending_smart_domains = {};
|
||||
@@ -290,6 +292,7 @@ let last_reload_time = 0;
|
||||
let proxy_consecutive_fails = 0;
|
||||
let dns_consecutive_fails = 0;
|
||||
let ai_healthy_streak = 0;
|
||||
let cached_proxy_port = null;
|
||||
let proxy_latency_history = [];
|
||||
let dns_latency_history = [];
|
||||
let last_anomaly_check = 0;
|
||||
@@ -465,12 +468,20 @@ function safe_proxy_restart(reason) {
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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) {}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -562,6 +573,9 @@ function ai_heal_proxy_connectivity() {
|
||||
|
||||
let now = time();
|
||||
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");
|
||||
if (sb_cfg_data) {
|
||||
try {
|
||||
@@ -569,13 +583,15 @@ function ai_heal_proxy_connectivity() {
|
||||
if (sb_cfg.inbounds) {
|
||||
for (let inb in sb_cfg.inbounds) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
let proxy_ok = command_success_from_args([
|
||||
"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() {
|
||||
if (!uloop) return null;
|
||||
syslog_start_time = time();
|
||||
@@ -1504,7 +1541,8 @@ function setup_syslog_listener() {
|
||||
// Without this, every restart cascades: new watchdog inherits old watchdog's
|
||||
// logread pipe read-end, keeping old logread alive. Over N restarts,
|
||||
// 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");
|
||||
if (!log_pipe) return null;
|
||||
// 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_dns_loop, "ai_heal_dns_loop");
|
||||
safe_call(analyze_anomalies, "analyze_anomalies");
|
||||
safe_call(recover_oom_scale, "recover_oom_scale");
|
||||
}
|
||||
|
||||
if (uloop) {
|
||||
@@ -1632,6 +1671,8 @@ function worker() {
|
||||
uloop.run();
|
||||
} else {
|
||||
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) {
|
||||
perform_fast_checks();
|
||||
perform_normal_checks();
|
||||
|
||||
@@ -32,7 +32,12 @@ function command_from_args(args) {
|
||||
|
||||
function command_status(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) {
|
||||
|
||||
@@ -121,18 +121,9 @@ function ensure_parent_dir(path) {
|
||||
}
|
||||
|
||||
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))
|
||||
return false;
|
||||
if (!write_json_file(tmp_path, value))
|
||||
return false;
|
||||
if (!fs.rename(tmp_path, path)) {
|
||||
fs.unlink(tmp_path);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return write_json_file(path, value);
|
||||
}
|
||||
|
||||
function fixture_section_list(type_name) {
|
||||
|
||||
@@ -45,7 +45,12 @@ function command_output_from_args(args) {
|
||||
|
||||
function command_status(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) {
|
||||
@@ -436,8 +441,16 @@ function process_running(pid) {
|
||||
|
||||
function stop_runtime() {
|
||||
let pid = file_first_line(PRIORITY_PID_FILE);
|
||||
if (process_running(pid))
|
||||
if (process_running(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);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -79,7 +79,12 @@ function command_output_from_args(args) {
|
||||
|
||||
function command_status(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) {
|
||||
|
||||
@@ -603,7 +603,12 @@ function command_success_from_args(args) {
|
||||
|
||||
function command_status_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) {
|
||||
|
||||
Reference in New Issue
Block a user