let fs = require("fs"); let uci_core = require("core.uci"); let common = require("core.common"); let constants = require("core.constants"); const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon"; const PAUSE_FILE = "/tmp/tachyon_paused_until"; const WATCHDOG_PID_FILE = "/var/run/tachyon_watchdog.pid"; let as_string = common.as_string; let command_capture = common.command_capture; let command_success_from_args = common.command_success_from_args; let command_from_args = common.command_from_args; let command_output = common.command_output; // Helper to get clash url function get_clash_url(endpoint) { let host = "127.0.0.1:9090"; // default fallback let config_data = fs.readfile("/etc/sing-box/config.json"); if (config_data) { try { let sb_cfg = json(config_data); let ext = sb_cfg.experimental?.clash_api?.external_controller; if (ext) { let parts = split(ext, ":"); let ip = (length(parts) > 1) ? parts[0] : ""; let port = (length(parts) > 1) ? parts[length(parts) - 1] : "9090"; if (ip == "0.0.0.0" || ip == "") { host = "127.0.0.1:" + port; } else { host = ext; } } } catch (e) { // Falls through to the default host below. The clash API is optional // and every caller already treats an unreachable one as "no data". } } return "http://" + host + "/" + endpoint; } // ─── API Functions ─────────────────────────────────────────────────────────── function get_pause_remaining() { let val = trim(fs.readfile(PAUSE_FILE) || ""); if (val == "") return 0; let until = int(val); let now = time(); if (until <= now) return 0; return until - now; } function process_running_by_pidfile(pidfile) { let pid = trim(fs.readfile(pidfile) || ""); return pid != "" && match(pid, /^[0-9]+$/) != null && command_success_from_args(["kill", "-0", pid]); } function get_clash_proxies_data() { let args = [ "curl", "-s", "-m", "5", "--connect-timeout", "3", get_clash_url("proxies") ]; let res = command_capture(command_from_args(args)); if (res && res.status == 0 && res.output != "") { try { return json(res.output); } catch (e) { warn("api: failed to parse proxies JSON: " + as_string(e) + "\n"); } } return null; } function get_system_status() { let status_obj = {}; // CPU let stat = fs.readfile("/proc/loadavg") || ""; let load = split(stat, / /); status_obj.cpu = (length(load) > 0) ? load[0] : "unknown"; // RAM let mem = fs.readfile("/proc/meminfo") || ""; let total_kb = 0, avail_kb = 0; for (let line in split(mem, "\n")) { if (index(line, "MemTotal:") == 0) { total_kb = int(split(trim(line), /[ \t]+/)[1]); } else if (index(line, "MemAvailable:") == 0) { avail_kb = int(split(trim(line), /[ \t]+/)[1]); } } status_obj.ram_total = total_kb ? int(total_kb / 1024) : 0; status_obj.ram_avail = avail_kb ? int(avail_kb / 1024) : 0; // Uptime let upt = fs.readfile("/proc/uptime") || ""; let up_sec = double(split(upt, / /)[0] || 0); let d = int(up_sec / 86400); let h = int((up_sec - (d * 86400)) / 3600); let m = int((up_sec - (d * 86400) - (h * 3600)) / 60); status_obj.uptime = sprintf("%d дн. %02d:%02d", d, h, m); // sing-box status let sb_running = command_success_from_args(["pidof", "sing-box"]); status_obj.singbox_running = sb_running; status_obj.singbox = sb_running ? "🟢 running" : "🔴 stopped"; let sb_variant = trim(as_string(fs.readfile("/etc/tachyon/sing-box-variant") || "")); if (sb_variant != "") status_obj.singbox_variant = sb_variant; // zapret2 status let z2_installed = fs.stat("/opt/zapret2/nfq2/nfqws2") != null; status_obj.zapret2_installed = z2_installed; if (z2_installed) { let z2_running = command_success_from_args(["pidof", "nfqws2"]); status_obj.zapret2_running = z2_running; status_obj.zapret2 = z2_running ? "🟢 running" : "🔴 stopped"; } status_obj.tachyon_running = command_success_from_args(["/etc/init.d/tachyon", "status"]); // Active servers for all Selector groups status_obj.active_servers = {}; let pdata = get_clash_proxies_data(); if (pdata && pdata.proxies) { for (let gname in keys(pdata.proxies)) { let p = pdata.proxies[gname]; if (p.type == "Selector" || p.type == "URLTest" || p.type == "Fallback") { if (p.now) { let srv_name = p.now; let latency = "N/A"; let srv = pdata.proxies[srv_name]; if (srv && type(srv.history) == "array" && length(srv.history) > 0) { let last = srv.history[length(srv.history) - 1]; if (last && last.delay) latency = last.delay; } status_obj.active_servers[gname] = { server: srv_name, latency: latency }; } } } } // Watchdog status status_obj.watchdog_running = process_running_by_pidfile(WATCHDOG_PID_FILE); // Pause state status_obj.pause_remaining = get_pause_remaining(); // IPs status_obj.lan_ip = as_string(common.command_output("uci -q get network.lan.ipaddr")); status_obj.wan_ip = as_string(common.command_output("ip route get 8.8.8.8 2>/dev/null | grep -Eo 'src [0-9.]+' | awk '{print $2}'")); // Tachyon Version let ver = "unknown"; let opkg_out = common.command_output("opkg status tachyon 2>/dev/null | grep Version"); if (opkg_out && trim(opkg_out) != "") { ver = split(trim(opkg_out), " ")[1]; } else { let apk_out = common.command_output("apk list tachyon 2>/dev/null"); if (apk_out && trim(apk_out) != "") { let m = match(apk_out, /tachyon-([0-9A-Za-z._+-]+)/); if (m && m[1]) ver = m[1]; } } status_obj.tachyon_version = ver || "unknown"; status_obj.tachyon_commit_sha = constants.TACHYON_COMMIT_SHA && !match(constants.TACHYON_COMMIT_SHA, /COMPILED/) ? constants.TACHYON_COMMIT_SHA : ""; return status_obj; } function clash_request(method, endpoint, payload) { let url = get_clash_url(endpoint); // ucode has no Math global (math is a separate loadable module), so the // microsecond field of clock() supplies the uniqueness suffix, the same way // temp paths are built elsewhere in this codebase. let payload_path = "/tmp/clash_payload_" + method + "_" + time() + "_" + clock()[1] + ".json"; let res = null; try { let args = [ "curl", "-s", "-m", "5", "--connect-timeout", "3", "-X", method ]; if (payload) { fs.writefile(payload_path, sprintf("%J", payload)); push(args, "-H", "Content-Type: application/json", "-d", "@" + payload_path); } push(args, url); res = command_capture(command_from_args(args)); } catch (e) { if (payload) { try { fs.unlink(payload_path); } catch(err) {} } return null; } if (payload) { try { fs.unlink(payload_path); } catch(err) {} } if (!res || res.status != 0 || res.output == "") { return null; } try { return json(res.output); } catch (e) { return res.output; } } function get_clash_connections() { let args = [ "curl", "-s", "-m", "5", "--connect-timeout", "3", get_clash_url("connections") ]; let res = command_capture(command_from_args(args)); if (res && res.status == 0 && res.output != "") { try { return json(res.output); } catch (e) { // Falls through to null, which every caller already handles as "the // clash API gave us nothing" - the same result as a failed curl. } } return null; } function http_probe(use_proxy) { // curl exits 0 for any completed HTTP exchange, including 4xx/5xx, so the // status code has to be inspected explicitly. let cmd = "curl -I -s -o /dev/null -m 8 --connect-timeout 4 -w '%{http_code}' " + (use_proxy ? "--proxy http://127.0.0.1:" + common.get_mixed_port() + " " : "") + "https://www.google.com"; let res = command_capture(cmd); if (!res || res.status != 0) return false; let code = int(trim(as_string(res.output || "0"))); return code >= 200 && code < 400; } function check_connection() { return { direct: http_probe(false), proxy: http_probe(true) }; } function run_speedtest() { // Direct speedtest let res_direct = command_capture("curl -s -m 15 --connect-timeout 6 -w '%{speed_download}' -o /dev/null https://speed.cloudflare.com/__down?bytes=5242880"); let direct_speed = (res_direct && res_direct.status == 0) ? double(res_direct.output || 0) : 0; let direct_mbps = (direct_speed * 8) / 1000000; // Proxy speedtest let res_proxy = command_capture("curl -s -m 15 --connect-timeout 6 --proxy http://127.0.0.1:" + common.get_mixed_port() + " -w '%{speed_download}' -o /dev/null https://speed.cloudflare.com/__down?bytes=5242880"); let proxy_speed = (res_proxy && res_proxy.status == 0) ? double(res_proxy.output || 0) : 0; let proxy_mbps = (proxy_speed * 8) / 1000000; return { direct_mbps, proxy_mbps, direct_ok: direct_speed > 0, proxy_ok: proxy_speed > 0 }; } function manage_domain_list(action_type, domain, do_delete) { let c = uci_core.cursor(); if (!c) return { success: false, error: "Не удалось инициализировать UCI" }; c.load(CONFIG_NAME); let target_section = null; c.foreach(CONFIG_NAME, "section", function(s) { if (s.enabled == "1" && s.action == action_type) { target_section = s[".name"]; return false; } }); if (!target_section) { return { success: false, error: "Не найдено активное правило с действием '" + action_type + "'." }; } return manage_domain_list_by_section(target_section, domain, do_delete); } function manage_domain_list_by_section(sec_name, domain, do_delete) { let c = uci_core.cursor(); if (!c) return { success: false, error: "Не удалось инициализировать UCI" }; c.load(CONFIG_NAME); let s = c.get_all(CONFIG_NAME, sec_name); if (!s) { return { success: false, error: "Секция '" + sec_name + "' не найдена." }; } let option_path = CONFIG_NAME + "." + sec_name + ".domain"; if (do_delete) { if (uci_core.del_list(option_path, domain)) { uci_core.commit(CONFIG_NAME); common.command_status("/usr/bin/tachyon reload"); return { success: true, message: "Запись " + domain + " удалена." }; } else { return { success: false, error: "Запись " + domain + " не найдена." }; } } else { let current_val = uci_core.get(option_path); let exists = false; if (current_val != "") { for (let item in split(current_val, " ")) { if (item == domain) { exists = true; break; } } } if (exists) { return { success: false, error: "Запись " + domain + " уже есть в секции." }; } if (uci_core.add_list(option_path, domain)) { uci_core.commit(CONFIG_NAME); common.command_status("/usr/bin/tachyon reload"); return { success: true, message: "Запись " + domain + " добавлена." }; } return { success: false, error: "Не удалось добавить запись." }; } } function get_sections() { let c = uci_core.cursor(); if (!c) return []; c.load(CONFIG_NAME); let sections = []; c.foreach(CONFIG_NAME, "section", function(s) { push(sections, s); }); return sections; } function get_servers() { let c = uci_core.cursor(); if (!c) return []; c.load(CONFIG_NAME); let servers = []; c.foreach(CONFIG_NAME, "server", function(s) { push(servers, s); }); return servers; } function reload_tachyon() { return common.command_status("/usr/bin/tachyon reload") == 0; } function toggle_section(sec_name) { let c = uci_core.cursor(); if (!c) return false; c.load(CONFIG_NAME); let s = c.get_all(CONFIG_NAME, sec_name); if (!s) return false; let new_val = (s.enabled == "1") ? "0" : "1"; uci_core.set(CONFIG_NAME + "." + sec_name + ".enabled", new_val); uci_core.commit(CONFIG_NAME); reload_tachyon(); return true; } return { get_pause_remaining, process_running_by_pidfile, get_system_status, get_clash_proxies_data, clash_request, get_clash_connections, check_connection, run_speedtest, manage_domain_list, manage_domain_list_by_section, get_sections, get_servers, reload_tachyon, toggle_section };