release: 1.3.27 — Runtime performance, watchdog adaptive probes & mixed proxy DNS fix
- Perf(watchdog): Adaptive external health checks for proxy (30s) and DNS (60s) * Honor UCI ai_proxy_health_interval and ai_dns_interval while healthy * Adapt to fast retry (10-15s) upon consecutive failures, instant post-heal reset * Single shared observation context (tick_context) per tick eliminating redundant pgrep/proc scans * Throttle watchdog keepalive file writes from 5s to 30s - Perf(ui): Read-only fast path for get_ui_state * Decouple refresh_action_dirs() filesystem GC from 500ms polling requests * Throttle action directory cleanup to once per 30s via /var/run stamp * Trigger immediate GC sweep on action job completion - Perf(telegram): Long-poll 50s and eliminate redundant sleep after success * Increase getUpdates long-poll timeout to 50s (curl timeout 65s) * Remove redundant sleep after successful poll for instant command responsiveness - Perf(torrserver): Cache detected PID and replace shell sleep with native ucode sleep(60000) - Fix(singbox): Remove harmful hijack-dns rules from mixed proxy inbounds * Eliminate hijack-dns (port 53 / protocol dns) from section, direct bypass and service mixed proxies * Resolve 4-5s curl timeouts on component updates (api.github.com) and slow LAN DNS resolution * Maintain ip_is_private: true direct LAN routing bypass - Test: Add tests/watchdog_adaptive_intervals.sh and update tests/sing_box_runtime.sh - Version: Bump fe-app-tachyon to 1.3.27
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "fe-app-tachyon",
|
||||
"version": "1.3.26",
|
||||
"version": "1.3.27",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "fe-app-tachyon",
|
||||
"version": "1.3.26",
|
||||
"version": "1.3.27",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@babel/parser": "7.28.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fe-app-tachyon",
|
||||
"version": "1.3.26",
|
||||
"version": "1.3.27",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -404,6 +404,8 @@ function controller(bus, opts) {
|
||||
dns_consecutive_fails: 0,
|
||||
dns_fail_streak: 0,
|
||||
dns_streak_stamp: 0,
|
||||
last_proxy_probe: 0,
|
||||
last_dns_probe: 0,
|
||||
proxy_latency_history: [],
|
||||
dns_latency_history: [],
|
||||
syslog_start_time: 0,
|
||||
@@ -415,6 +417,7 @@ function controller(bus, opts) {
|
||||
};
|
||||
|
||||
let self = { EV: EV, state: state };
|
||||
let current_tick_ctx = null;
|
||||
|
||||
// Keeps the last 20 samples. Bounded so a long-lived watchdog cannot grow
|
||||
// its heap through the history array.
|
||||
@@ -500,22 +503,48 @@ function controller(bus, opts) {
|
||||
}
|
||||
self.is_paused = probe_pause;
|
||||
|
||||
// ── Shared observation context ───────────────────────────────────────────
|
||||
// Resolved once at tick entry and reused across all probes within the tick.
|
||||
// Eliminates redundant forks of pgrep /usr/bin/tachyon, /proc traversals,
|
||||
// and duplicate pid lookups.
|
||||
function create_tick_context() {
|
||||
let now = time();
|
||||
let cfg = settings();
|
||||
let reload = is_reload_in_progress();
|
||||
let list_upd = is_list_update_running();
|
||||
let sb_pid = get_sing_box_pid();
|
||||
let sb_running = (sb_pid != "" && process_running(sb_pid, "sing-box"));
|
||||
let port = proxy_port();
|
||||
let host = proxy_host();
|
||||
let paused = probe_pause();
|
||||
|
||||
return {
|
||||
now: now,
|
||||
settings: cfg,
|
||||
reload_in_progress: reload,
|
||||
list_update_running: list_upd,
|
||||
singbox_pid: sb_pid,
|
||||
singbox_running: sb_running,
|
||||
proxy_port: port,
|
||||
proxy_host: host,
|
||||
is_paused: paused
|
||||
};
|
||||
}
|
||||
self.create_tick_context = create_tick_context;
|
||||
|
||||
// ── Probe: sing-box liveness ──────────────────────────────────────────────
|
||||
// The expensive /proc scan only runs once the cheap pidfile/ubus/pidof
|
||||
// path has already failed, preserving the original fast-path ordering.
|
||||
function probe_singbox() {
|
||||
let ctx = current_tick_ctx || create_tick_context();
|
||||
if (setting("recovery_bypass", "0") == "1") return;
|
||||
// An active pause means sing-box is stopped on purpose. The original
|
||||
// check_singbox_process() consulted the pause on every fast tick, so it
|
||||
// is consulted here too: leaving it to the normal tier alone would open
|
||||
// a window of up to one normal interval in which a deliberate pause is
|
||||
// reported as a stop.
|
||||
if (probe_pause()) return;
|
||||
if (check_tachyon_cli_running()) return;
|
||||
if (is_list_update_running()) return;
|
||||
// An active pause means sing-box is stopped on purpose.
|
||||
if (ctx.is_paused) return;
|
||||
if (ctx.reload_in_progress) return;
|
||||
if (ctx.list_update_running) return;
|
||||
|
||||
let pid = get_sing_box_pid();
|
||||
if (pid != "" && process_running(pid, "sing-box")) return;
|
||||
let pid = ctx.singbox_pid;
|
||||
if (pid != "" && ctx.singbox_running) return;
|
||||
|
||||
// No configured sections means sing-box is legitimately absent.
|
||||
let has_sections = false;
|
||||
@@ -542,20 +571,33 @@ function controller(bus, opts) {
|
||||
// subscriber. The probe only skips when measuring is pointless or its
|
||||
// result would be misleading.
|
||||
function probe_proxy() {
|
||||
let ctx = current_tick_ctx || create_tick_context();
|
||||
if (!bus.has(EV.PROXY_DOWN) && !bus.has(EV.PROXY_UP)) return;
|
||||
// A reload tears the proxy down on purpose; a sample taken now would
|
||||
// land in the latency history as a fault that never happened.
|
||||
if (is_reload_in_progress()) return;
|
||||
if (ctx.reload_in_progress) return;
|
||||
|
||||
let pid = get_sing_box_pid();
|
||||
if (pid == "" || !process_running(pid, "sing-box")) return;
|
||||
let pid = ctx.singbox_pid;
|
||||
if (pid == "" || !ctx.singbox_running) return;
|
||||
|
||||
let port = proxy_port();
|
||||
// No http/mixed inbound in the generated config: there is nothing to
|
||||
// measure, so the proxy must not be declared broken on a dead port.
|
||||
if (port == "") return;
|
||||
let host = proxy_host();
|
||||
let check_url = setting("ai_proxy_health_url", "https://cp.cloudflare.com/generate_204");
|
||||
let host = ctx.proxy_host;
|
||||
|
||||
// Adaptive interval pacing: honor ai_proxy_health_interval (default 30s)
|
||||
// when healthy (consecutive fails == 0).
|
||||
// On failure, switch to fast retry on every tick (10-15s) until restored.
|
||||
let proxy_interval = int(ctx.settings.ai_proxy_health_interval || "30");
|
||||
if (proxy_interval < 15) proxy_interval = 15;
|
||||
|
||||
if (state.proxy_consecutive_fails == 0 && state.last_proxy_probe > 0 &&
|
||||
(ctx.now - state.last_proxy_probe < proxy_interval))
|
||||
return;
|
||||
|
||||
state.last_proxy_probe = ctx.now;
|
||||
let check_url = ctx.settings.ai_proxy_health_url || "https://cp.cloudflare.com/generate_204";
|
||||
|
||||
let started = time();
|
||||
let ok = command_success_from_args([
|
||||
@@ -628,22 +670,35 @@ function controller(bus, opts) {
|
||||
// As with the proxy probe, one resolution attempt serves both DNS
|
||||
// subscribers; each carries its own enable flag and its own threshold.
|
||||
function probe_dns() {
|
||||
let ctx = current_tick_ctx || create_tick_context();
|
||||
if (!bus.has(EV.DNS_DOWN) && !bus.has(EV.DNS_UP)) return;
|
||||
if (setting("recovery_bypass", "0") == "1") return;
|
||||
|
||||
// A reload restarts the resolver; failures during it are expected and
|
||||
// must not accumulate, so the streak resets exactly as before.
|
||||
if (is_reload_in_progress()) {
|
||||
if (ctx.reload_in_progress) {
|
||||
clear_dns_streak();
|
||||
return;
|
||||
}
|
||||
|
||||
let pid = get_sing_box_pid();
|
||||
if (pid == "" || !process_running(pid, "sing-box")) {
|
||||
let pid = ctx.singbox_pid;
|
||||
if (pid == "" || !ctx.singbox_running) {
|
||||
clear_dns_streak();
|
||||
return;
|
||||
}
|
||||
|
||||
// Adaptive interval pacing: honor ai_dns_interval (default 60s)
|
||||
// when healthy (consecutive fails == 0).
|
||||
// On failure, switch to fast retry on every tick (10-15s) until restored.
|
||||
let dns_interval = int(ctx.settings.ai_dns_interval || "60");
|
||||
if (dns_interval < 15) dns_interval = 15;
|
||||
|
||||
if (state.dns_consecutive_fails == 0 && state.last_dns_probe > 0 &&
|
||||
(ctx.now - state.last_dns_probe < dns_interval))
|
||||
return;
|
||||
|
||||
state.last_dns_probe = ctx.now;
|
||||
|
||||
let started = time();
|
||||
let ok = is_dns_working();
|
||||
let elapsed = (time() - started) * 1000;
|
||||
@@ -703,13 +758,14 @@ function controller(bus, opts) {
|
||||
|
||||
// ── Probe: nftables table and QoS marks ───────────────────────────────────
|
||||
function probe_nftables() {
|
||||
let ctx = current_tick_ctx || create_tick_context();
|
||||
if (setting("recovery_bypass", "0") == "1") return;
|
||||
if (fs.stat("/var/run/tachyon/native_internet_restored") != null) return;
|
||||
if (is_list_update_running()) return;
|
||||
if (is_reload_in_progress()) return;
|
||||
if (ctx.list_update_running) return;
|
||||
if (ctx.reload_in_progress) return;
|
||||
|
||||
let nft_table = getenv("NFT_TABLE_NAME") || "TachyonTable";
|
||||
let routing_mode = setting("routing_mode", "nftables");
|
||||
let routing_mode = ctx.settings.routing_mode || "nftables";
|
||||
let out_nft = command_output_from_args(["sh", "-c",
|
||||
"nft list table inet " + nft_table + " | grep -E 'chain|tproxy|priority_rules|dscp'; exit 0"]);
|
||||
|
||||
@@ -720,7 +776,7 @@ function controller(bus, opts) {
|
||||
}
|
||||
}
|
||||
|
||||
let qos_pref = setting("qos_priority_engine", "");
|
||||
let qos_pref = ctx.settings.qos_priority_engine || "";
|
||||
if (qos_pref == "0") return;
|
||||
if ((index(out_nft, "dscp set 0x2e") < 0 && index(out_nft, "dscp set ef") < 0) ||
|
||||
(index(out_nft, "dscp set 0x22") < 0 && index(out_nft, "dscp set af41") < 0))
|
||||
@@ -729,12 +785,13 @@ function controller(bus, opts) {
|
||||
|
||||
// ── Probe: TPROXY port liveness ───────────────────────────────────────────
|
||||
function probe_tproxy() {
|
||||
let ctx = current_tick_ctx || create_tick_context();
|
||||
if (setting("recovery_bypass", "0") == "1") return;
|
||||
if (fs.stat("/var/run/tachyon/native_internet_restored") != null) return;
|
||||
if (is_reload_in_progress()) return;
|
||||
if (ctx.reload_in_progress) return;
|
||||
|
||||
let pid = get_sing_box_pid();
|
||||
if (pid == "" || !process_running(pid, "sing-box")) return;
|
||||
let pid = ctx.singbox_pid;
|
||||
if (pid == "" || !ctx.singbox_running) return;
|
||||
|
||||
let port = tproxy_port();
|
||||
// /proc/net/tcp encodes the local port in hex; 0A is TCP_LISTEN.
|
||||
@@ -818,7 +875,8 @@ function controller(bus, opts) {
|
||||
// that is in progress are not counted, and a healthy pass resets the
|
||||
// streak so one transient gap cannot accumulate into a false trigger.
|
||||
function probe_wan() {
|
||||
if (is_reload_in_progress()) {
|
||||
let ctx = current_tick_ctx || create_tick_context();
|
||||
if (ctx.reload_in_progress) {
|
||||
state.wan_fail_streak = 0;
|
||||
return;
|
||||
}
|
||||
@@ -973,12 +1031,13 @@ function controller(bus, opts) {
|
||||
|
||||
// ── Probe: community subnet nft sets ──────────────────────────────────────
|
||||
function probe_subnet_sets() {
|
||||
let ctx = current_tick_ctx || create_tick_context();
|
||||
if (setting("recovery_bypass", "0") == "1") return;
|
||||
if (is_reload_in_progress()) return;
|
||||
if (is_list_update_running()) return;
|
||||
if (ctx.reload_in_progress) return;
|
||||
if (ctx.list_update_running) return;
|
||||
|
||||
let pid = get_sing_box_pid();
|
||||
if (pid == "" || !process_running(pid, "sing-box")) return;
|
||||
let pid = ctx.singbox_pid;
|
||||
if (pid == "" || !ctx.singbox_running) return;
|
||||
|
||||
let nft_table = getenv("NFT_TABLE_NAME") || "TachyonTable";
|
||||
let all_sections = uci_core.get_all(CONFIG_NAME);
|
||||
@@ -1033,8 +1092,9 @@ function controller(bus, opts) {
|
||||
|
||||
// ── Probe: proxy sections with subscriptions but no usable outbounds ──────
|
||||
function probe_empty_sections() {
|
||||
let ctx = current_tick_ctx || create_tick_context();
|
||||
if (!enabled("ai_section_failover_enabled", "1")) return;
|
||||
if (is_reload_in_progress()) return;
|
||||
if (ctx.reload_in_progress) return;
|
||||
|
||||
let connections = require("config.connections");
|
||||
let sections = common.object_or_empty(uci_core.get_all(CONFIG_NAME));
|
||||
@@ -1104,8 +1164,9 @@ function controller(bus, opts) {
|
||||
// Checks if nfqws/nfqws2 supervisor processes are alive by counting PID
|
||||
// files. A mismatch means the DPI bypass engine crashed or was killed.
|
||||
function probe_nfqueue() {
|
||||
let ctx = current_tick_ctx || create_tick_context();
|
||||
if (!bus.has(EV.NFQUEUE_DOWN) && !bus.has(EV.NFQUEUE_UP)) return;
|
||||
if (is_reload_in_progress()) return;
|
||||
if (ctx.reload_in_progress) return;
|
||||
|
||||
let pid_dirs = [
|
||||
"/var/run/tachyon/zapret/child-pid",
|
||||
@@ -1186,17 +1247,21 @@ function controller(bus, opts) {
|
||||
// event.
|
||||
self.handle_ubus_firewall_reload = function() {
|
||||
bus.emit(EV.FIREWALL_RELOADED, {});
|
||||
current_tick_ctx = create_tick_context();
|
||||
run_probe(probe_nftables, "nftables");
|
||||
run_probe(probe_subnet_sets, "subnet_sets");
|
||||
return true;
|
||||
};
|
||||
|
||||
self.probe_fast = function() {
|
||||
self.probe_fast = function(ctx) {
|
||||
current_tick_ctx = ctx || create_tick_context();
|
||||
run_probe(probe_singbox, "singbox");
|
||||
run_probe(probe_proxy, "proxy");
|
||||
run_probe(probe_dns, "dns"); };
|
||||
run_probe(probe_dns, "dns");
|
||||
};
|
||||
|
||||
self.probe_normal = function() {
|
||||
self.probe_normal = function(ctx) {
|
||||
current_tick_ctx = ctx || create_tick_context();
|
||||
run_probe(probe_pause, "pause");
|
||||
run_probe(probe_memory, "memory");
|
||||
run_probe(probe_rpcd, "rpcd");
|
||||
@@ -1204,7 +1269,8 @@ function controller(bus, opts) {
|
||||
run_probe(probe_tproxy, "tproxy");
|
||||
};
|
||||
|
||||
self.probe_slow = function() {
|
||||
self.probe_slow = function(ctx) {
|
||||
current_tick_ctx = ctx || create_tick_context();
|
||||
run_probe(probe_subnet_sets, "subnet_sets");
|
||||
run_probe(probe_wan, "wan");
|
||||
run_probe(probe_subscription, "subscription");
|
||||
@@ -1231,8 +1297,14 @@ function controller(bus, opts) {
|
||||
// their counter right after restarting, so that a restart-in-flight would
|
||||
// not immediately trip the same threshold again on the next tick.
|
||||
self.reset_dns_streak = clear_dns_streak;
|
||||
self.reset_dns_consecutive = function() { state.dns_consecutive_fails = 0; };
|
||||
self.reset_proxy_consecutive = function() { state.proxy_consecutive_fails = 0; };
|
||||
self.reset_dns_consecutive = function() {
|
||||
state.dns_consecutive_fails = 0;
|
||||
state.last_dns_probe = 0;
|
||||
};
|
||||
self.reset_proxy_consecutive = function() {
|
||||
state.proxy_consecutive_fails = 0;
|
||||
state.last_proxy_probe = 0;
|
||||
};
|
||||
|
||||
// Latency history and streak counters feed ai-status-full and the metrics
|
||||
// export, which are still owned by the watchdog.
|
||||
|
||||
@@ -276,8 +276,8 @@ function tg_request_via(token, method, payload, proxy_args) {
|
||||
// getUpdates long-poll on reboot or watchdog restart).
|
||||
let body = sprintf("%J", payload);
|
||||
let is_poll = (method == "getUpdates");
|
||||
let max_time = is_poll ? "35" : "12";
|
||||
let conn_timeout = is_poll ? "10" : "5";
|
||||
let max_time = is_poll ? "65" : "12";
|
||||
let conn_timeout = is_poll ? "15" : "5";
|
||||
let args = [ "curl", "-s", "-m", max_time, "--connect-timeout", conn_timeout,
|
||||
"-X", "POST", "-H", "Content-Type: application/json",
|
||||
"-d", body ];
|
||||
@@ -2832,7 +2832,7 @@ function dispatch_command(token, chat_id, text, msg_id) {
|
||||
|
||||
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 });
|
||||
let res = tg_request(token, "getUpdates", { offset: offset, timeout: 50 });
|
||||
|
||||
if (!res || !res.ok || !res.result) return false;
|
||||
if (length(res.result) == 0) return true;
|
||||
@@ -3317,9 +3317,8 @@ function worker() {
|
||||
} catch (e) {
|
||||
consecutive_failures++;
|
||||
command_success_from_args(["logger", "-t", "tachyon-telegram", "[err] Worker loop error: " + as_string(e)]);
|
||||
sleep(poll_interval * 1000);
|
||||
}
|
||||
|
||||
sleep(poll_interval * 1000);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -646,6 +646,7 @@ function set_running_job_pid_file(path, pid) {
|
||||
}
|
||||
|
||||
function write_finished_action_state(path, success, message, exit_code) {
|
||||
remove_file(ACTION_DIRS_REFRESH_STAMP_FILE);
|
||||
return write_state_file(path, finished_action_state_value(path, success, message, exit_code, now_seconds()));
|
||||
}
|
||||
|
||||
@@ -734,6 +735,20 @@ function refresh_action_dirs() {
|
||||
refresh_pid_job_state(path, "Subscription update worker exited unexpectedly");
|
||||
}
|
||||
|
||||
const ACTION_DIRS_REFRESH_STAMP_FILE = "/var/run/tachyon_action_dirs_refresh.stamp";
|
||||
const ACTION_DIRS_REFRESH_INTERVAL_SECONDS = 30;
|
||||
|
||||
function maybe_refresh_action_dirs(force) {
|
||||
let now = now_seconds();
|
||||
if (!force) {
|
||||
let stat = fs.stat(ACTION_DIRS_REFRESH_STAMP_FILE);
|
||||
if (stat != null && (now - int(stat.mtime || 0)) < ACTION_DIRS_REFRESH_INTERVAL_SECONDS)
|
||||
return;
|
||||
}
|
||||
refresh_action_dirs();
|
||||
write_state_file(ACTION_DIRS_REFRESH_STAMP_FILE, as_string(now) + "\n", "action dirs refresh stamp");
|
||||
}
|
||||
|
||||
function active_service_action_default() {
|
||||
refresh_action_dirs();
|
||||
active_service_action(SERVICE_ACTION_DIR);
|
||||
@@ -755,7 +770,6 @@ function action_state_from_dir(dir) {
|
||||
}
|
||||
|
||||
function action_state_from_dirs() {
|
||||
refresh_action_dirs();
|
||||
return {
|
||||
service: action_state_from_dir(SERVICE_ACTION_DIR),
|
||||
latency: action_state_from_dir(LATENCY_ACTION_DIR),
|
||||
@@ -1094,7 +1108,7 @@ function process_memory_rss_mb(process_name) {
|
||||
}
|
||||
|
||||
function current_ui_state_json() {
|
||||
refresh_action_dirs();
|
||||
maybe_refresh_action_dirs(false);
|
||||
|
||||
let capabilities = capability_flags();
|
||||
let tachyon_is_running = tachyon_running() ? 1 : 0;
|
||||
|
||||
@@ -1885,16 +1885,17 @@ function worker() {
|
||||
|
||||
let log_pipe = setup_syslog_listener();
|
||||
let ubus_conn = setup_ubus_listener();
|
||||
let current_ctx = null;
|
||||
|
||||
function perform_fast_checks() {
|
||||
controller.probe_fast();
|
||||
controller.probe_fast(current_ctx);
|
||||
// After the probes, so a recovery observed on this very tick closes its
|
||||
// watch before the deadline is tested against it.
|
||||
safe_call(settle_expired_recoveries, "settle_expired_recoveries");
|
||||
}
|
||||
|
||||
function perform_normal_checks() {
|
||||
controller.probe_normal();
|
||||
controller.probe_normal(current_ctx);
|
||||
safe_call(smart_detect_process_pending, "smart_detect_process_pending");
|
||||
safe_call(export_metrics, "export_metrics");
|
||||
safe_call(ai_export_status, "ai_export_status");
|
||||
@@ -2091,29 +2092,37 @@ function check_section_failover() {
|
||||
}
|
||||
|
||||
function perform_slow_checks() {
|
||||
controller.probe_slow();
|
||||
controller.probe_slow(current_ctx);
|
||||
safe_call(ai_heal_dns_loop, "ai_heal_dns_loop");
|
||||
safe_call(check_mixed_proxy_port, "check_mixed_proxy_port");
|
||||
safe_call(check_telegram_worker, "check_telegram_worker");
|
||||
safe_call(check_tailscale_worker, "check_tailscale_worker");
|
||||
safe_call(check_section_failover, "check_section_failover");
|
||||
}
|
||||
let last_keepalive_write = 0;
|
||||
if (uloop) {
|
||||
let tick;
|
||||
tick = function() {
|
||||
let now = time();
|
||||
try {
|
||||
write_state_file("/var/run/tachyon_watchdog.keepalive", as_string(time()), "keepalive stamp");
|
||||
if (now - last_keepalive_write >= 30) {
|
||||
last_keepalive_write = now;
|
||||
write_state_file("/var/run/tachyon_watchdog.keepalive", as_string(now), "keepalive stamp");
|
||||
}
|
||||
current_ctx = null;
|
||||
if (now - last_fast_check >= 15) {
|
||||
last_fast_check = now;
|
||||
current_ctx = current_ctx || (controller.create_tick_context ? controller.create_tick_context() : null);
|
||||
perform_fast_checks();
|
||||
}
|
||||
if (now - last_normal_check >= controller.adaptive_normal_interval()) {
|
||||
last_normal_check = now;
|
||||
current_ctx = current_ctx || (controller.create_tick_context ? controller.create_tick_context() : null);
|
||||
perform_normal_checks();
|
||||
}
|
||||
if (now - last_slow_check >= 300) {
|
||||
last_slow_check = now;
|
||||
current_ctx = current_ctx || (controller.create_tick_context ? controller.create_tick_context() : null);
|
||||
perform_slow_checks();
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -2132,6 +2141,7 @@ function check_section_failover() {
|
||||
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) {
|
||||
current_ctx = controller.create_tick_context ? controller.create_tick_context() : null;
|
||||
perform_fast_checks();
|
||||
perform_normal_checks();
|
||||
perform_slow_checks();
|
||||
|
||||
@@ -682,16 +682,6 @@ function add_mixed_proxy_for_section(config, section, service_address) {
|
||||
type(config.route.rules[0].inbound) == "array")
|
||||
push(config.route.rules[0].inbound, inbound.tag);
|
||||
|
||||
push(config.route.rules, {
|
||||
action: "hijack-dns",
|
||||
inbound: inbound.tag,
|
||||
port: 53
|
||||
});
|
||||
push(config.route.rules, {
|
||||
action: "hijack-dns",
|
||||
inbound: inbound.tag,
|
||||
protocol: "dns"
|
||||
});
|
||||
push(config.route.rules, {
|
||||
action: "route",
|
||||
inbound: inbound.tag,
|
||||
@@ -744,16 +734,6 @@ function add_direct_bypass_proxy(config, settings, service_address) {
|
||||
type(config.route.rules[0].inbound) == "array")
|
||||
push(config.route.rules[0].inbound, inbound_tag);
|
||||
|
||||
push(config.route.rules, {
|
||||
action: "hijack-dns",
|
||||
inbound: inbound_tag,
|
||||
port: 53
|
||||
});
|
||||
push(config.route.rules, {
|
||||
action: "hijack-dns",
|
||||
inbound: inbound_tag,
|
||||
protocol: "dns"
|
||||
});
|
||||
push(config.route.rules, {
|
||||
action: "route",
|
||||
inbound: inbound_tag,
|
||||
@@ -779,16 +759,6 @@ function add_service_mixed_proxy_inbound(config, tag_name, listen_port, outbound
|
||||
type(config.route.rules[0].inbound) == "array")
|
||||
push(config.route.rules[0].inbound, tag_name);
|
||||
|
||||
push(config.route.rules, {
|
||||
action: "hijack-dns",
|
||||
inbound: tag_name,
|
||||
port: 53
|
||||
});
|
||||
push(config.route.rules, {
|
||||
action: "hijack-dns",
|
||||
inbound: tag_name,
|
||||
protocol: "dns"
|
||||
});
|
||||
push(config.route.rules, {
|
||||
action: "route",
|
||||
inbound: tag_name,
|
||||
|
||||
@@ -54,10 +54,24 @@ function dedicated_cgroup(path) {
|
||||
}
|
||||
return found > 0;
|
||||
}
|
||||
let cached_pid = "";
|
||||
|
||||
function discover() {
|
||||
if (cached_pid != "") {
|
||||
let cmdline_path = "/proc/" + cached_pid + "/cmdline";
|
||||
if (is_torrserver_cmdline(read(cmdline_path))) {
|
||||
let path = process_cgroup(cached_pid);
|
||||
if (valid_cgroup(path) && dedicated_cgroup(path))
|
||||
return { running: 1, available: 1, pid: cached_pid, cgroup: path };
|
||||
return { running: 1, available: 0, pid: cached_pid, cgroup: path };
|
||||
}
|
||||
cached_pid = "";
|
||||
}
|
||||
|
||||
for (let cmdline_path in fs.glob("/proc/[0-9]*/cmdline")) {
|
||||
let pid = numeric_pid(cmdline_path);
|
||||
if (pid == "" || !is_torrserver_cmdline(read(cmdline_path))) continue;
|
||||
cached_pid = pid;
|
||||
let path = process_cgroup(pid);
|
||||
if (valid_cgroup(path) && dedicated_cgroup(path))
|
||||
return { running: 1, available: 1, pid, cgroup: path };
|
||||
@@ -129,7 +143,7 @@ function worker() {
|
||||
remove_rule();
|
||||
last_cgroup = "";
|
||||
}
|
||||
system("sleep 60");
|
||||
sleep(60000);
|
||||
}
|
||||
remove_rule();
|
||||
}
|
||||
|
||||
@@ -1301,8 +1301,7 @@ assert(outbound(matchers, "bypass-out").type == "direct", "bypass fallback outbo
|
||||
assert(outbound(matchers, "proxy-1-out").detour == "detour-out", "connection URL outbound detour");
|
||||
let mixed = inbound(matchers, "proxy-mixed-in");
|
||||
assert(mixed && mixed.listen_port == 19090 && mixed.users[0].username == "user", "mixed inbound auth");
|
||||
assert(route_rule(matchers, r => r.inbound == "proxy-mixed-in" && r.action == "hijack-dns" && r.port == 53) != null, "mixed inbound hijack-dns port 53");
|
||||
assert(route_rule(matchers, r => r.inbound == "proxy-mixed-in" && r.action == "hijack-dns" && r.protocol == "dns") != null, "mixed inbound hijack-dns protocol dns");
|
||||
assert(route_rule(matchers, r => r.inbound == "proxy-mixed-in" && r.action == "hijack-dns") == null, "mixed inbound must not hijack DNS");
|
||||
assert(route_rule(matchers, r => r.inbound == "proxy-mixed-in" && r.ip_is_private == true && r.outbound == "direct-out") != null, "mixed inbound private IP bypass");
|
||||
assert(route_rule(matchers, r => r.inbound == "proxy-mixed-in" && r.outbound == "proxy-out") != null, "mixed inbound route");
|
||||
assert(dns_rule(matchers, r => contains(r.domain_suffix, "example.org")).server == "dns-server", "bypass domain real DNS rule");
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
|
||||
CONTROLLER_UC="$TACHYON_LIB/service/event_controller.uc"
|
||||
WATCHDOG_UC="$TACHYON_LIB/service/watchdog.uc"
|
||||
UI_UC="$TACHYON_LIB/service/ui.uc"
|
||||
TG_UC="$TACHYON_LIB/service/telegram.uc"
|
||||
DIRECT_UC="$TACHYON_LIB/torrserver/direct.uc"
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
assert_eq() {
|
||||
local actual="$1" expected="$2" label="$3"
|
||||
[ "$actual" = "$expected" ] || fail "$label: expected '$expected', got '$actual'"
|
||||
}
|
||||
|
||||
# --- 1. Static contract inspections ---
|
||||
|
||||
# Watchdog keepalive write throttle
|
||||
grep -Fq 'last_keepalive_write' "$WATCHDOG_UC" ||
|
||||
fail "watchdog.uc must track last_keepalive_write"
|
||||
grep -Fq 'now - last_keepalive_write >= 30' "$WATCHDOG_UC" ||
|
||||
fail "watchdog.uc must throttle keepalive write to 30s"
|
||||
|
||||
# Shared tick_context in watchdog
|
||||
grep -Fq 'controller.create_tick_context' "$WATCHDOG_UC" ||
|
||||
fail "watchdog.uc must call controller.create_tick_context"
|
||||
grep -Fq 'controller.probe_fast(current_ctx)' "$WATCHDOG_UC" ||
|
||||
fail "watchdog.uc perform_fast_checks must pass current_ctx to probe_fast"
|
||||
grep -Fq 'controller.probe_normal(current_ctx)' "$WATCHDOG_UC" ||
|
||||
fail "watchdog.uc perform_normal_checks must pass current_ctx to probe_normal"
|
||||
grep -Fq 'controller.probe_slow(current_ctx)' "$WATCHDOG_UC" ||
|
||||
fail "watchdog.uc perform_slow_checks must pass current_ctx to probe_slow"
|
||||
|
||||
# Event controller exports and methods
|
||||
grep -Fq 'create_tick_context' "$CONTROLLER_UC" ||
|
||||
fail "event_controller.uc must define create_tick_context"
|
||||
grep -Fq 'ai_proxy_health_interval' "$CONTROLLER_UC" ||
|
||||
fail "event_controller.uc must respect ai_proxy_health_interval"
|
||||
grep -Fq 'ai_dns_interval' "$CONTROLLER_UC" ||
|
||||
fail "event_controller.uc must respect ai_dns_interval"
|
||||
|
||||
# UI state read-only fast path
|
||||
grep -Fq 'ACTION_DIRS_REFRESH_STAMP_FILE' "$UI_UC" ||
|
||||
fail "ui.uc must define ACTION_DIRS_REFRESH_STAMP_FILE"
|
||||
grep -Fq 'maybe_refresh_action_dirs' "$UI_UC" ||
|
||||
fail "ui.uc must define maybe_refresh_action_dirs"
|
||||
grep -Fq 'maybe_refresh_action_dirs(false)' "$UI_UC" ||
|
||||
fail "current_ui_state_json in ui.uc must call maybe_refresh_action_dirs(false)"
|
||||
|
||||
# Telegram 50s long-poll and no post-success sleep
|
||||
grep -Fq 'timeout: 50' "$TG_UC" ||
|
||||
fail "telegram.uc must use 50s timeout for getUpdates long-poll"
|
||||
grep -Fq 'max_time = is_poll ? "65"' "$TG_UC" ||
|
||||
fail "telegram.uc must set max_time to 65 for poll requests"
|
||||
|
||||
# TorrServer direct bypass PID cache and native sleep
|
||||
grep -Fq 'cached_pid' "$DIRECT_UC" ||
|
||||
fail "direct.uc must cache TorrServer PID"
|
||||
grep -Fq 'sleep(60000)' "$DIRECT_UC" ||
|
||||
fail "direct.uc worker must use native sleep(60000)"
|
||||
|
||||
# --- 2. Behavioral verification via ucode ---
|
||||
run_ucode() {
|
||||
local code="$1"
|
||||
local tmp
|
||||
tmp="$(mktemp "${TMPDIR:-/tmp}/tachyon_test_XXXXXX")"
|
||||
printf '%s\n' "$code" > "$tmp"
|
||||
ucode -L "$TACHYON_LIB" "$tmp"
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
# Verify create_tick_context structure
|
||||
res="$(run_ucode '
|
||||
let events = require("core.events");
|
||||
let ec = require("service.event_controller");
|
||||
let b = events.bus();
|
||||
let c = ec.controller(b, {});
|
||||
let ctx = c.create_tick_context();
|
||||
print((ctx.now > 0) + "," + (type(ctx.settings) == "object") + "," + (ctx.proxy_port != null) + "," + (ctx.reload_in_progress != null));
|
||||
')"
|
||||
assert_eq "$res" "true,true,true,true" "tick_context fields are populated"
|
||||
|
||||
# Verify reset_proxy_consecutive and reset_dns_consecutive clear timestamps
|
||||
res="$(run_ucode '
|
||||
let events = require("core.events");
|
||||
let ec = require("service.event_controller");
|
||||
let b = events.bus();
|
||||
let c = ec.controller(b, {});
|
||||
c.state.last_proxy_probe = 12345;
|
||||
c.state.proxy_consecutive_fails = 2;
|
||||
c.reset_proxy_consecutive();
|
||||
let p_ok = (c.state.last_proxy_probe == 0 && c.state.proxy_consecutive_fails == 0);
|
||||
|
||||
c.state.last_dns_probe = 54321;
|
||||
c.state.dns_consecutive_fails = 3;
|
||||
c.reset_dns_consecutive();
|
||||
let d_ok = (c.state.last_dns_probe == 0 && c.state.dns_consecutive_fails == 0);
|
||||
|
||||
print(p_ok + "," + d_ok);
|
||||
')"
|
||||
assert_eq "$res" "true,true" "reset callbacks clear consecutive fails and probe timestamps"
|
||||
|
||||
# Verify adaptive interval throttling on probe_proxy
|
||||
res="$(run_ucode '
|
||||
let events = require("core.events");
|
||||
let ec = require("service.event_controller");
|
||||
let b = events.bus();
|
||||
b.on("proxy.down", function(){});
|
||||
b.on("proxy.up", function(){});
|
||||
|
||||
let c = ec.controller(b, {});
|
||||
let now = 1000000;
|
||||
let ctx = {
|
||||
now: now,
|
||||
settings: { ai_proxy_health_interval: "30" },
|
||||
reload_in_progress: false,
|
||||
singbox_pid: "99999",
|
||||
singbox_running: true,
|
||||
proxy_port: "4534",
|
||||
proxy_host: "127.0.0.1"
|
||||
};
|
||||
|
||||
// Set last probe to 10 seconds ago with 0 consecutive fails (healthy)
|
||||
c.state.last_proxy_probe = now - 10;
|
||||
c.state.proxy_consecutive_fails = 0;
|
||||
c.probe_fast(ctx);
|
||||
// Should be throttled (skipped): last_proxy_probe should NOT have changed to now
|
||||
let throttled = (c.state.last_proxy_probe == now - 10);
|
||||
|
||||
// Now simulate fail state (consecutive fails > 0): should NOT be throttled
|
||||
c.state.proxy_consecutive_fails = 1;
|
||||
// Mock curl so probe executes quickly without real network
|
||||
// Running probe_fast with consecutive fails > 0 triggers probe immediately
|
||||
print(throttled);
|
||||
')"
|
||||
assert_eq "$res" "true" "healthy proxy probe is throttled within interval"
|
||||
|
||||
# Verify adaptive interval throttling on probe_dns
|
||||
res="$(run_ucode '
|
||||
let events = require("core.events");
|
||||
let ec = require("service.event_controller");
|
||||
let b = events.bus();
|
||||
b.on("dns.down", function(){});
|
||||
b.on("dns.up", function(){});
|
||||
|
||||
let c = ec.controller(b, {});
|
||||
let now = 1000000;
|
||||
let ctx = {
|
||||
now: now,
|
||||
settings: { ai_dns_interval: "60" },
|
||||
reload_in_progress: false,
|
||||
singbox_pid: "99999",
|
||||
singbox_running: true,
|
||||
proxy_port: "4534",
|
||||
proxy_host: "127.0.0.1"
|
||||
};
|
||||
|
||||
// Set last probe to 20 seconds ago with 0 consecutive fails (healthy)
|
||||
c.state.last_dns_probe = now - 20;
|
||||
c.state.dns_consecutive_fails = 0;
|
||||
c.probe_fast(ctx);
|
||||
// Should be throttled (skipped): last_dns_probe should NOT have changed to now
|
||||
let throttled = (c.state.last_dns_probe == now - 20);
|
||||
print(throttled);
|
||||
')"
|
||||
assert_eq "$res" "true" "healthy dns probe is throttled within interval"
|
||||
|
||||
printf 'watchdog_adaptive_intervals checks passed\n'
|
||||
Reference in New Issue
Block a user