fix(updates): hoist reload_singbox_after_list_update and persist compiled rulesets to /etc/tachyon/rulesets
This commit is contained in:
@@ -2329,9 +2329,19 @@ function ensure_ruleset_source(path) {
|
||||
}
|
||||
|
||||
function cleanup_empty_ruleset(path) {
|
||||
if (routing_rulesets_module().has_rules(path))
|
||||
let slash = rindex(path, "/");
|
||||
let filename = slash >= 0 ? substr(path, slash + 1) : path;
|
||||
let persistent_path = "/etc/tachyon/rulesets/" + filename;
|
||||
if (routing_rulesets_module().has_rules(path)) {
|
||||
if (filename != "") {
|
||||
ensure_dir("/etc/tachyon/rulesets");
|
||||
copy_file(path, persistent_path);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
remove_file(path);
|
||||
if (filename != "")
|
||||
remove_file(persistent_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2787,6 +2797,7 @@ function import_domains_from_remote_domain_lists(section, settings) {
|
||||
if (!import_domains_from_remote_plain_file(url, section, settings))
|
||||
ok = false;
|
||||
}
|
||||
cleanup_empty_ruleset(remote_ruleset_path(section, "domains"));
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -2885,6 +2896,7 @@ function import_subnets_from_remote_subnet_lists(section, settings) {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
cleanup_empty_ruleset(remote_ruleset_path(section, "subnets"));
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -3003,6 +3015,113 @@ function update_list_status(running, success, progress, message) {
|
||||
});
|
||||
}
|
||||
|
||||
function subscription_cache_env() {
|
||||
return {
|
||||
TACHYON_CONFIG_NAME: CONFIG_NAME,
|
||||
TACHYON_LIB: LIB_DIR,
|
||||
TMP_SING_BOX_FOLDER,
|
||||
TMP_RULESET_FOLDER,
|
||||
TMP_SUBSCRIPTION_FOLDER,
|
||||
TACHYON_RUNTIME_STATE_DIR: RUNTIME_STATE_DIR,
|
||||
TACHYON_SUBSCRIPTION_UPDATE_STATE_DIR: SUBSCRIPTION_UPDATE_STATE_DIR,
|
||||
TACHYON_SUBSCRIPTION_LINKS_DIR: SUBSCRIPTION_LINKS_DIR,
|
||||
TACHYON_SUBSCRIPTION_METADATA_DIR: SUBSCRIPTION_METADATA_DIR,
|
||||
TACHYON_OUTBOUND_METADATA_DIR: OUTBOUND_METADATA_DIR,
|
||||
TACHYON_SECTION_CACHE_DIR: SECTION_CACHE_DIR,
|
||||
TACHYON_RUNTIME_CACHE_FORMAT_FILE: RUNTIME_CACHE_FORMAT_FILE,
|
||||
TACHYON_RUNTIME_CACHE_FORMAT: RUNTIME_CACHE_FORMAT,
|
||||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_DIR: PERSISTENT_SUBSCRIPTION_CACHE_DIR,
|
||||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE: PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE,
|
||||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT: PERSISTENT_SUBSCRIPTION_CACHE_FORMAT,
|
||||
TACHYON_PENDING_RELOAD_FILE: PENDING_RELOAD_FILE,
|
||||
TACHYON_SERVICE_INIT: SERVICE_INIT,
|
||||
SB_SERVICE_MIXED_INBOUND_ADDRESS,
|
||||
SB_SERVICE_MIXED_INBOUND_PORT,
|
||||
SB_VARIANT_STATE_FILE
|
||||
};
|
||||
}
|
||||
|
||||
function module_env_capture(env, args) {
|
||||
let output_path = temp_path();
|
||||
if (output_path == "")
|
||||
return { status: 1, output: "" };
|
||||
|
||||
let status = command_status(command_env(env) + " " + module_command(args) + " >" + shell_quote(output_path) + " 2>&1");
|
||||
let output = as_string(fs.readfile(output_path) || "");
|
||||
remove_file(output_path);
|
||||
return { status, output };
|
||||
}
|
||||
|
||||
function subscription_cache_capture(args) {
|
||||
let command_args = [ LIB_DIR + "/subscription/cache.uc" ];
|
||||
for (let arg in args)
|
||||
push(command_args, arg);
|
||||
return module_env_capture(subscription_cache_env(), command_args);
|
||||
}
|
||||
|
||||
function subscription_cache_success(args) {
|
||||
let result = subscription_cache_capture(args);
|
||||
if (result.output != "")
|
||||
for (let line in split(result.output, "\n"))
|
||||
if (trim(as_string(line)) != "")
|
||||
log_message("subscription cache: " + line, "debug");
|
||||
return result.status == 0;
|
||||
}
|
||||
|
||||
function log_file_lines_from_text(text, level, prefix) {
|
||||
for (let line in split(as_string(text), "\n"))
|
||||
if (trim(as_string(line)) != "")
|
||||
log_message(as_string(prefix) + as_string(line), level);
|
||||
}
|
||||
|
||||
function singbox_runtime_success(args) {
|
||||
let command_args = [ LIB_DIR + "/singbox/runtime.uc" ];
|
||||
for (let arg in args)
|
||||
push(command_args, arg);
|
||||
|
||||
let result = module_env_capture(subscription_cache_env(), command_args);
|
||||
if (result.output != "")
|
||||
log_file_lines_from_text(result.output, "debug", "sing-box runtime: ");
|
||||
return result.status == 0;
|
||||
}
|
||||
|
||||
function write_current_reload_state_clean() {
|
||||
return service_state_success([
|
||||
"write-current-reload-state-clean",
|
||||
RELOAD_STATE_FILE,
|
||||
RELOAD_STATE_FORMAT,
|
||||
RULE_CONDITION_CACHE_DIR
|
||||
]);
|
||||
}
|
||||
|
||||
function reload_singbox_after_list_update() {
|
||||
let sing_box_pid = trim(module_output([ LIB_DIR + "/service/state.uc", "sing-box-service-runtime-pid" ]));
|
||||
if (sing_box_pid == "" || int(sing_box_pid) <= 0)
|
||||
return true;
|
||||
let sing_box_config_path = option(uci_settings(), "config_path", "") || "/etc/sing-box/config.json";
|
||||
let sing_box_config_hash_before = file_md5(sing_box_config_path);
|
||||
if (!singbox_runtime_success([ "init-config", "0", "1", "1" ]))
|
||||
return false;
|
||||
let sing_box_config_hash_after = file_md5(sing_box_config_path);
|
||||
if (sing_box_config_hash_before != sing_box_config_hash_after) {
|
||||
log_message("Rulesets updated on disk; reloading sing-box with new configuration", "info");
|
||||
module_success([ DNS_FAILOVER_UC, "stop-runtime" ]);
|
||||
module_success([ PRIORITY_UC, "stop-runtime" ]);
|
||||
let ok = service_state_success([
|
||||
"reload-sing-box-runtime",
|
||||
sing_box_pid,
|
||||
sing_box_config_hash_before,
|
||||
sing_box_config_hash_after,
|
||||
"1"
|
||||
]);
|
||||
module_success([ PRIORITY_UC, "start-runtime" ]);
|
||||
module_success([ DNS_FAILOVER_UC, "start-runtime" ]);
|
||||
write_current_reload_state_clean();
|
||||
return ok;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function list_update() {
|
||||
log_message("Starting lists update", "info");
|
||||
if (!list_update_pid_begin())
|
||||
@@ -3172,113 +3291,6 @@ function stop_list_update() {
|
||||
remove_file(LIST_UPDATE_PID_FILE);
|
||||
}
|
||||
|
||||
function subscription_cache_env() {
|
||||
return {
|
||||
TACHYON_CONFIG_NAME: CONFIG_NAME,
|
||||
TACHYON_LIB: LIB_DIR,
|
||||
TMP_SING_BOX_FOLDER,
|
||||
TMP_RULESET_FOLDER,
|
||||
TMP_SUBSCRIPTION_FOLDER,
|
||||
TACHYON_RUNTIME_STATE_DIR: RUNTIME_STATE_DIR,
|
||||
TACHYON_SUBSCRIPTION_UPDATE_STATE_DIR: SUBSCRIPTION_UPDATE_STATE_DIR,
|
||||
TACHYON_SUBSCRIPTION_LINKS_DIR: SUBSCRIPTION_LINKS_DIR,
|
||||
TACHYON_SUBSCRIPTION_METADATA_DIR: SUBSCRIPTION_METADATA_DIR,
|
||||
TACHYON_OUTBOUND_METADATA_DIR: OUTBOUND_METADATA_DIR,
|
||||
TACHYON_SECTION_CACHE_DIR: SECTION_CACHE_DIR,
|
||||
TACHYON_RUNTIME_CACHE_FORMAT_FILE: RUNTIME_CACHE_FORMAT_FILE,
|
||||
TACHYON_RUNTIME_CACHE_FORMAT: RUNTIME_CACHE_FORMAT,
|
||||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_DIR: PERSISTENT_SUBSCRIPTION_CACHE_DIR,
|
||||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE: PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE,
|
||||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT: PERSISTENT_SUBSCRIPTION_CACHE_FORMAT,
|
||||
TACHYON_PENDING_RELOAD_FILE: PENDING_RELOAD_FILE,
|
||||
TACHYON_SERVICE_INIT: SERVICE_INIT,
|
||||
SB_SERVICE_MIXED_INBOUND_ADDRESS,
|
||||
SB_SERVICE_MIXED_INBOUND_PORT,
|
||||
SB_VARIANT_STATE_FILE
|
||||
};
|
||||
}
|
||||
|
||||
function module_env_capture(env, args) {
|
||||
let output_path = temp_path();
|
||||
if (output_path == "")
|
||||
return { status: 1, output: "" };
|
||||
|
||||
let status = command_status(command_env(env) + " " + module_command(args) + " >" + shell_quote(output_path) + " 2>&1");
|
||||
let output = as_string(fs.readfile(output_path) || "");
|
||||
remove_file(output_path);
|
||||
return { status, output };
|
||||
}
|
||||
|
||||
function subscription_cache_capture(args) {
|
||||
let command_args = [ LIB_DIR + "/subscription/cache.uc" ];
|
||||
for (let arg in args)
|
||||
push(command_args, arg);
|
||||
return module_env_capture(subscription_cache_env(), command_args);
|
||||
}
|
||||
|
||||
function subscription_cache_success(args) {
|
||||
let result = subscription_cache_capture(args);
|
||||
if (result.output != "")
|
||||
for (let line in split(result.output, "\n"))
|
||||
if (trim(as_string(line)) != "")
|
||||
log_message("subscription cache: " + line, "debug");
|
||||
return result.status == 0;
|
||||
}
|
||||
|
||||
function log_file_lines_from_text(text, level, prefix) {
|
||||
for (let line in split(as_string(text), "\n"))
|
||||
if (trim(as_string(line)) != "")
|
||||
log_message(as_string(prefix) + as_string(line), level);
|
||||
}
|
||||
|
||||
function singbox_runtime_success(args) {
|
||||
let command_args = [ LIB_DIR + "/singbox/runtime.uc" ];
|
||||
for (let arg in args)
|
||||
push(command_args, arg);
|
||||
|
||||
let result = module_env_capture(subscription_cache_env(), command_args);
|
||||
if (result.output != "")
|
||||
log_file_lines_from_text(result.output, "debug", "sing-box runtime: ");
|
||||
return result.status == 0;
|
||||
}
|
||||
|
||||
function write_current_reload_state_clean() {
|
||||
return service_state_success([
|
||||
"write-current-reload-state-clean",
|
||||
RELOAD_STATE_FILE,
|
||||
RELOAD_STATE_FORMAT,
|
||||
RULE_CONDITION_CACHE_DIR
|
||||
]);
|
||||
}
|
||||
|
||||
function reload_singbox_after_list_update() {
|
||||
let sing_box_pid = trim(module_output([ LIB_DIR + "/service/state.uc", "sing-box-service-runtime-pid" ]));
|
||||
if (sing_box_pid == "" || int(sing_box_pid) <= 0)
|
||||
return true;
|
||||
let sing_box_config_path = option(uci_settings(), "config_path", "") || "/etc/sing-box/config.json";
|
||||
let sing_box_config_hash_before = file_md5(sing_box_config_path);
|
||||
if (!singbox_runtime_success([ "init-config", "0", "1", "1" ]))
|
||||
return false;
|
||||
let sing_box_config_hash_after = file_md5(sing_box_config_path);
|
||||
if (sing_box_config_hash_before != sing_box_config_hash_after) {
|
||||
log_message("Rulesets updated on disk; reloading sing-box with new configuration", "info");
|
||||
module_success([ DNS_FAILOVER_UC, "stop-runtime" ]);
|
||||
module_success([ PRIORITY_UC, "stop-runtime" ]);
|
||||
let ok = service_state_success([
|
||||
"reload-sing-box-runtime",
|
||||
sing_box_pid,
|
||||
sing_box_config_hash_before,
|
||||
sing_box_config_hash_after,
|
||||
"1"
|
||||
]);
|
||||
module_success([ PRIORITY_UC, "start-runtime" ]);
|
||||
module_success([ DNS_FAILOVER_UC, "start-runtime" ]);
|
||||
write_current_reload_state_clean();
|
||||
return ok;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function mark_pending_reload(reason) {
|
||||
service_state_success([ "mark-pending-reload", PENDING_RELOAD_FILE, reason ]);
|
||||
}
|
||||
|
||||
@@ -663,6 +663,14 @@ function dscp_value(section) {
|
||||
return list_option_value_from_array(dscp_list(section));
|
||||
}
|
||||
|
||||
function contains(arr, val) {
|
||||
if (type(arr) != "array") return false;
|
||||
for (let item in arr) {
|
||||
if (item == val) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function routed_dns_enabled(section) {
|
||||
return bool_option(section, "routed_dns_enabled", false);
|
||||
}
|
||||
@@ -683,14 +691,6 @@ function routed_dns_servers(section) {
|
||||
return servers;
|
||||
}
|
||||
|
||||
function contains(arr, val) {
|
||||
if (type(arr) != "array") return false;
|
||||
for (let item in arr) {
|
||||
if (item == val) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function geoip_country_list(section) {
|
||||
let raw = whitespace_list_value(section, "geoip_country");
|
||||
let result = [];
|
||||
|
||||
@@ -2156,6 +2156,10 @@ function is_container_process(pid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function uci_settings() {
|
||||
return uci_core.get_all(CONFIG_NAME, "settings") || {};
|
||||
}
|
||||
|
||||
function is_adguardhome_primary_dns(cfg) {
|
||||
let agh_pid = find_process_pid("AdGuardHome");
|
||||
if (agh_pid == "")
|
||||
@@ -2187,10 +2191,6 @@ function is_degraded() {
|
||||
return fs.stat("/tmp/tachyon/degraded") != null;
|
||||
}
|
||||
|
||||
function uci_settings() {
|
||||
return uci_core.get_all(CONFIG_NAME, "settings") || {};
|
||||
}
|
||||
|
||||
function kill_our_core_processes() {
|
||||
command_status("/etc/init.d/sing-box stop >/dev/null 2>&1");
|
||||
command_status("killall -9 sing-box >/dev/null 2>&1");
|
||||
|
||||
@@ -134,23 +134,23 @@ function validate_section(section) {
|
||||
|
||||
let provider = as_string(section.provider || "");
|
||||
if (provider != "" && !array_contains(OLCRTC_PROVIDERS, provider))
|
||||
errors.push("provider must be one of: " + join(", ", OLCRTC_PROVIDERS));
|
||||
push(errors, "provider must be one of: " + join(", ", OLCRTC_PROVIDERS));
|
||||
|
||||
let transport = as_string(section.transport || "");
|
||||
if (transport != "" && !array_contains(OLCRTC_TRANSPORTS, transport))
|
||||
errors.push("transport must be one of: " + join(", ", OLCRTC_TRANSPORTS));
|
||||
push(errors, "transport must be one of: " + join(", ", OLCRTC_TRANSPORTS));
|
||||
|
||||
let crypto_key = as_string(section.crypto_key || "");
|
||||
if (crypto_key != "" && !valid_hex_key(crypto_key))
|
||||
errors.push("crypto_key must be 64 hex characters");
|
||||
push(errors, "crypto_key must be 64 hex characters");
|
||||
|
||||
let socks_port = as_string(section.socks_port || "");
|
||||
if (socks_port != "" && !valid_number(socks_port, 1, 65535))
|
||||
errors.push("socks_port must be 1-65535");
|
||||
push(errors, "socks_port must be 1-65535");
|
||||
|
||||
let dns_server = as_string(section.dns_server || "");
|
||||
if (dns_server != "" && !valid_host_port(dns_server))
|
||||
errors.push("dns_server must be in HOST:PORT format");
|
||||
push(errors, "dns_server must be in HOST:PORT format");
|
||||
|
||||
let subscription_links = list_values(section.subscription_links);
|
||||
for (let link in subscription_links) {
|
||||
@@ -160,26 +160,15 @@ function validate_section(section) {
|
||||
if (substr(link, 0, 9) == "olcrtc://") {
|
||||
let parsed = parse_uri(link);
|
||||
if (!parsed.valid)
|
||||
errors.push("invalid olcrtc:// URI: " + parsed.reason);
|
||||
push(errors, "invalid olcrtc:// URI: " + parsed.reason);
|
||||
}
|
||||
else if (!valid_url(link))
|
||||
errors.push("subscription link must be olcrtc:// URI or HTTP URL: " + link);
|
||||
push(errors, "subscription link must be olcrtc:// URI or HTTP URL: " + link);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validate_section_json() {
|
||||
let input = read_stdin();
|
||||
let data = object_or_empty(json(input));
|
||||
let errors = validate_section(data);
|
||||
if (length(errors) > 0)
|
||||
write_json({ valid: false, message: join("; ", errors) });
|
||||
else
|
||||
write_json({ valid: true, message: "" });
|
||||
return length(errors) == 0;
|
||||
}
|
||||
|
||||
function read_stdin() {
|
||||
let input = fs.open("/dev/stdin", "r");
|
||||
if (!input)
|
||||
@@ -193,6 +182,17 @@ function write_json(value) {
|
||||
printf("%s\n", sprintf("%J", value));
|
||||
}
|
||||
|
||||
function validate_section_json() {
|
||||
let input = read_stdin();
|
||||
let data = object_or_empty(json(input));
|
||||
let errors = validate_section(data);
|
||||
if (length(errors) > 0)
|
||||
write_json({ valid: false, message: join("; ", errors) });
|
||||
else
|
||||
write_json({ valid: true, message: "" });
|
||||
return length(errors) == 0;
|
||||
}
|
||||
|
||||
return {
|
||||
validate_section,
|
||||
validate_section_json,
|
||||
|
||||
@@ -150,27 +150,27 @@ function validate_section(section) {
|
||||
|
||||
let peer = as_string(section.peer || "");
|
||||
if (peer != "" && !valid_peer(peer))
|
||||
errors.push("peer must be in HOST:PORT format");
|
||||
push(errors, "peer must be in HOST:PORT format");
|
||||
|
||||
let mode = as_string(section.mode || "");
|
||||
if (!valid_mode(mode))
|
||||
errors.push("mode must be one of: " + join(", ", WDTT_MODES));
|
||||
push(errors, "mode must be one of: " + join(", ", WDTT_MODES));
|
||||
|
||||
let qwdtt_mode = as_string(section.qwdtt_mode || "");
|
||||
if (!valid_qwdtt_mode(qwdtt_mode))
|
||||
errors.push("qwdtt_mode must be one of: " + join(", ", QWDTT_MODES));
|
||||
push(errors, "qwdtt_mode must be one of: " + join(", ", QWDTT_MODES));
|
||||
|
||||
let workers = as_string(section.workers || "");
|
||||
if (workers != "" && !valid_number(workers, 1, 1024))
|
||||
errors.push("workers must be 1-1024");
|
||||
push(errors, "workers must be 1-1024");
|
||||
|
||||
let max_hashes = as_string(section.max_hashes || "");
|
||||
if (max_hashes != "" && !valid_number(max_hashes, 1, 64))
|
||||
errors.push("max_hashes must be 1-64");
|
||||
push(errors, "max_hashes must be 1-64");
|
||||
|
||||
let mtu = as_string(section.mtu || "");
|
||||
if (mtu != "" && !valid_number(mtu, 576, 1500))
|
||||
errors.push("mtu must be 576-1500");
|
||||
push(errors, "mtu must be 576-1500");
|
||||
|
||||
let subscription_links = list_values(section.subscription_links);
|
||||
for (let link in subscription_links) {
|
||||
@@ -180,35 +180,24 @@ function validate_section(section) {
|
||||
if (substr(link, 0, 7) == "wdtt://") {
|
||||
let parsed = parse_wdtt_uri(link);
|
||||
if (!parsed.valid)
|
||||
errors.push("invalid wdtt:// URI: " + parsed.reason);
|
||||
push(errors, "invalid wdtt:// URI: " + parsed.reason);
|
||||
}
|
||||
else if (substr(link, 0, 1) == "/") {
|
||||
/* local file — ok */
|
||||
}
|
||||
else if (!valid_url(link))
|
||||
errors.push("subscription link must be HTTP URL, wdtt:// URI, or local file path: " + link);
|
||||
push(errors, "subscription link must be HTTP URL, wdtt:// URI, or local file path: " + link);
|
||||
}
|
||||
|
||||
let community_lists = list_values(section.community_lists);
|
||||
for (let cl in community_lists) {
|
||||
if (!valid_community_list(cl))
|
||||
errors.push("unknown community list: " + cl);
|
||||
push(errors, "unknown community list: " + cl);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validate_section_json() {
|
||||
let input = read_stdin();
|
||||
let data = object_or_empty(json(input));
|
||||
let errors = validate_section(data);
|
||||
if (length(errors) > 0)
|
||||
write_json({ valid: false, message: join("; ", errors) });
|
||||
else
|
||||
write_json({ valid: true, message: "" });
|
||||
return length(errors) == 0;
|
||||
}
|
||||
|
||||
function read_stdin() {
|
||||
let input = fs.open("/dev/stdin", "r");
|
||||
if (!input)
|
||||
@@ -222,6 +211,17 @@ function write_json(value) {
|
||||
printf("%s\n", sprintf("%J", value));
|
||||
}
|
||||
|
||||
function validate_section_json() {
|
||||
let input = read_stdin();
|
||||
let data = object_or_empty(json(input));
|
||||
let errors = validate_section(data);
|
||||
if (length(errors) > 0)
|
||||
write_json({ valid: false, message: join("; ", errors) });
|
||||
else
|
||||
write_json({ valid: true, message: "" });
|
||||
return length(errors) == 0;
|
||||
}
|
||||
|
||||
return {
|
||||
validate_section,
|
||||
validate_section_json,
|
||||
|
||||
@@ -78,12 +78,6 @@ function get_agent_token() {
|
||||
return as_string(c.get(CONFIG_NAME + ".settings.agent_api_token") || "");
|
||||
}
|
||||
|
||||
function check_write_auth(bearer) {
|
||||
let token = get_agent_token();
|
||||
if (token == "") return false;
|
||||
return constant_time_eq("Bearer " + token, bearer);
|
||||
}
|
||||
|
||||
// Byte-wise comparison without early exit so response timing does not leak
|
||||
// how many leading token characters matched.
|
||||
function constant_time_eq(a, b) {
|
||||
@@ -100,6 +94,12 @@ function constant_time_eq(a, b) {
|
||||
return diff == 0;
|
||||
}
|
||||
|
||||
function check_write_auth(bearer) {
|
||||
let token = get_agent_token();
|
||||
if (token == "") return false;
|
||||
return constant_time_eq("Bearer " + token, bearer);
|
||||
}
|
||||
|
||||
// ─── READ endpoints ───────────────────────────────────────────────────────────
|
||||
|
||||
function handle_health() {
|
||||
|
||||
@@ -290,26 +290,6 @@ function run_leak_check_status(job_id) {
|
||||
return leak_mod.get_leak_check_status(job_id);
|
||||
}
|
||||
|
||||
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" };
|
||||
@@ -354,6 +334,26 @@ function manage_domain_list_by_section(sec_name, domain, do_delete) {
|
||||
}
|
||||
}
|
||||
|
||||
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 get_sections() {
|
||||
let c = uci_core.cursor();
|
||||
if (!c) return [];
|
||||
|
||||
@@ -174,6 +174,15 @@ function lock_dir_write_owner(lock_dir, owner_pid) {
|
||||
return write_text_file(as_string(lock_dir) + "/pid", as_string(owner_pid) + "\n");
|
||||
}
|
||||
|
||||
function release_runtime_dir_lock(lock_dir) {
|
||||
lock_dir = as_string(lock_dir);
|
||||
if (lock_dir == "")
|
||||
return;
|
||||
|
||||
command_success_from_args([ "rm", "-f", lock_dir + "/pid" ]);
|
||||
command_success_from_args([ "rmdir", lock_dir ]);
|
||||
}
|
||||
|
||||
function acquire_runtime_dir_lock(lock_dir, owner_pid) {
|
||||
lock_dir = as_string(lock_dir);
|
||||
owner_pid = as_string(owner_pid);
|
||||
@@ -222,15 +231,6 @@ function acquire_runtime_dir_lock(lock_dir, owner_pid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function release_runtime_dir_lock(lock_dir) {
|
||||
lock_dir = as_string(lock_dir);
|
||||
if (lock_dir == "")
|
||||
return;
|
||||
|
||||
command_success_from_args([ "rm", "-f", lock_dir + "/pid" ]);
|
||||
command_success_from_args([ "rmdir", lock_dir ]);
|
||||
}
|
||||
|
||||
function mark_pending_reload(path, reason) {
|
||||
path = as_string(path || PENDING_RELOAD_FILE);
|
||||
reason = as_string(reason || "pending");
|
||||
@@ -410,8 +410,14 @@ function restore_dnsmasq_failsafe() {
|
||||
return module_status(DNS_APPLY_UC, [ "failsafe-restore" ]);
|
||||
}
|
||||
|
||||
function begin_external_service_action(action, source, owner_pid) {
|
||||
if (as_string(getenv("TACHYON_UI_ACTION_TRACKED") || "0") == "1")
|
||||
function owner_pid_value() {
|
||||
let pid = trim(command_output_from_args([ "sh", "-c", "echo $PPID" ]));
|
||||
return match(pid, /^[0-9]+$/) != null ? pid : "0";
|
||||
}
|
||||
|
||||
function begin_external_service_action(action, owner_pid, source) {
|
||||
action = as_string(action);
|
||||
if (action == "")
|
||||
return "";
|
||||
if (!file_exists(UI_UC))
|
||||
return "";
|
||||
@@ -429,11 +435,6 @@ function finish_external_service_action(action, job_id, status) {
|
||||
return module_status(UI_UC, [ "service-action-finish-after-command", action, job_id, as_string(status) ]);
|
||||
}
|
||||
|
||||
function owner_pid_value() {
|
||||
let pid = trim(command_output_from_args([ "sh", "-c", "echo $PPID" ]));
|
||||
return match(pid, /^[0-9]+$/) != null ? pid : "0";
|
||||
}
|
||||
|
||||
function runtime_status_object() {
|
||||
let data = command_output_from_args([ BIN_PATH, "get_status" ]);
|
||||
try {
|
||||
|
||||
@@ -257,6 +257,15 @@ function lock_dir_write_owner(lock_dir, owner_pid) {
|
||||
return write_text_file(as_string(lock_dir) + "/pid", as_string(owner_pid) + "\n");
|
||||
}
|
||||
|
||||
function release_runtime_dir_lock(lock_dir) {
|
||||
lock_dir = as_string(lock_dir);
|
||||
if (lock_dir == "")
|
||||
return;
|
||||
|
||||
command_success_from_args([ "rm", "-f", lock_dir + "/pid" ]);
|
||||
command_success_from_args([ "rmdir", lock_dir ]);
|
||||
}
|
||||
|
||||
function acquire_runtime_dir_lock(lock_dir, owner_pid) {
|
||||
lock_dir = as_string(lock_dir);
|
||||
owner_pid = as_string(owner_pid);
|
||||
@@ -320,15 +329,6 @@ function acquire_runtime_dir_lock_wait(lock_dir, owner_pid, timeout) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function release_runtime_dir_lock(lock_dir) {
|
||||
lock_dir = as_string(lock_dir);
|
||||
if (lock_dir == "")
|
||||
return;
|
||||
|
||||
command_success_from_args([ "rm", "-f", lock_dir + "/pid" ]);
|
||||
command_success_from_args([ "rmdir", lock_dir ]);
|
||||
}
|
||||
|
||||
// The two unlinks below clean up the temporary file after a failed write or
|
||||
// rename. Both paths exit(1) immediately after, so the failure is already
|
||||
// reported; an unlink that throws means the temp file was never created.
|
||||
|
||||
@@ -266,27 +266,6 @@ function rotate_log_if_needed() {
|
||||
fs.writefile(LOG_FILE, "--- log truncated (size cap) ---\n" + tail);
|
||||
}
|
||||
|
||||
function alert_route_failure(attempt, proxy_alive) {
|
||||
// One-shot per failure episode - the caller clears the flag as soon as
|
||||
// polling recovers. Delivery rides on tg_request's own fallback chain:
|
||||
// if the proxy route is what's broken, the message goes out directly.
|
||||
let cfg = settings();
|
||||
let token = trim(as_string(cfg.bot_token || ""));
|
||||
if (token == "") return;
|
||||
let admins = split(trim(as_string(cfg.admin_ids || "")), /,/);
|
||||
let chat_id = trim(as_string(admins[0] || ""));
|
||||
if (chat_id == "") return;
|
||||
|
||||
let hint = proxy_alive
|
||||
? t("route_proxy_alive")
|
||||
: t("route_proxy_down");
|
||||
let text = "⚠️ <b>" + t("route_alert_title") + "</b> " + as_string(attempt) +
|
||||
" " + t("route_alert_consecutive") + ".\n" +
|
||||
t("route_reason") + " " + hint + ".\n" +
|
||||
t("route_fallback_hint") + "\n" +
|
||||
t("route_heal_hint");
|
||||
send_message(token, chat_id, text, "HTML", null);
|
||||
}
|
||||
|
||||
function tg_request_via(token, method, payload, proxy_args) {
|
||||
if (!token) return null;
|
||||
@@ -350,6 +329,28 @@ function send_message(token, chat_id, text, parse_mode, keyboard) {
|
||||
return res;
|
||||
}
|
||||
|
||||
function alert_route_failure(attempt, proxy_alive) {
|
||||
// One-shot per failure episode - the caller clears the flag as soon as
|
||||
// polling recovers. Delivery rides on tg_request's own fallback chain:
|
||||
// if the proxy route is what's broken, the message goes out directly.
|
||||
let cfg = settings();
|
||||
let token = trim(as_string(cfg.bot_token || ""));
|
||||
if (token == "") return;
|
||||
let admins = split(trim(as_string(cfg.admin_ids || "")), /,/);
|
||||
let chat_id = trim(as_string(admins[0] || ""));
|
||||
if (chat_id == "") return;
|
||||
|
||||
let hint = proxy_alive
|
||||
? t("route_proxy_alive")
|
||||
: t("route_proxy_down");
|
||||
let text = "⚠️ <b>" + t("route_alert_title") + "</b> " + as_string(attempt) +
|
||||
" " + t("route_alert_consecutive") + ".\n" +
|
||||
t("route_reason") + " " + hint + ".\n" +
|
||||
t("route_fallback_hint") + "\n" +
|
||||
t("route_heal_hint");
|
||||
send_message(token, chat_id, text, "HTML", null);
|
||||
}
|
||||
|
||||
function edit_message(token, chat_id, message_id, text, parse_mode, keyboard) {
|
||||
text = as_string(text);
|
||||
if (length(text) > 3900) {
|
||||
@@ -2269,50 +2270,6 @@ function find_mac_block_rules(c, mac) {
|
||||
return found;
|
||||
}
|
||||
|
||||
function handle_toggle_mac(token, chat_id, mac_raw) {
|
||||
let mac = normalize_mac(mac_raw);
|
||||
if (!mac) {
|
||||
send_message(token, chat_id, "❌ " + t("mac_invalid"), "HTML");
|
||||
return view_devices(token, chat_id, null);
|
||||
}
|
||||
|
||||
let c = uci_core.cursor();
|
||||
if (!c) {
|
||||
return send_message(token, chat_id, "❌ " + t("mac_firewall_open_failed"), "HTML",
|
||||
[[{ text: "⬅️ " + t("nav_menu"), callback_data: "/menu" }]]);
|
||||
}
|
||||
c.load("firewall");
|
||||
|
||||
let existing = find_mac_block_rules(c, mac);
|
||||
let blocked_now;
|
||||
if (length(existing) > 0) {
|
||||
for (let name in existing)
|
||||
c.delete("firewall", name);
|
||||
blocked_now = false;
|
||||
} else {
|
||||
let sec = c.add("firewall", "rule");
|
||||
if (!sec) {
|
||||
return send_message(token, chat_id, "❌ " + t("mac_block_rule_failed"), "HTML",
|
||||
[[{ text: "⬅️ " + t("nav_menu"), callback_data: "/menu" }]]);
|
||||
}
|
||||
c.set("firewall", sec, "name", "Tachyon block " + mac);
|
||||
c.set("firewall", sec, "src", "lan");
|
||||
c.set("firewall", sec, "dest", "*");
|
||||
c.set("firewall", sec, "proto", "all");
|
||||
c.set("firewall", sec, "src_mac", mac);
|
||||
c.set("firewall", sec, "target", "REJECT");
|
||||
blocked_now = true;
|
||||
}
|
||||
c.commit("firewall");
|
||||
// Reload in the background so the poll loop is not blocked by fw4
|
||||
system(common.background_command("/etc/init.d/firewall reload"));
|
||||
|
||||
send_message(token, chat_id,
|
||||
(blocked_now ? "🚫 Устройство <code>" : "🔓 Устройство <code>") + mac +
|
||||
(blocked_now ? "</code> заблокировано." : "</code> разблокировано.") +
|
||||
"\n\n<i>Правила firewall применяются в фоне (несколько секунд).</i>", "HTML");
|
||||
return view_devices(token, chat_id, null);
|
||||
}
|
||||
|
||||
function view_devices(token, chat_id, msg_id) {
|
||||
let lease_file = "/tmp/dhcp.leases";
|
||||
@@ -2362,6 +2319,51 @@ function view_devices(token, chat_id, msg_id) {
|
||||
else send_message(token, chat_id, text, "HTML", keyboard);
|
||||
}
|
||||
|
||||
function handle_toggle_mac(token, chat_id, mac_raw) {
|
||||
let mac = normalize_mac(mac_raw);
|
||||
if (!mac) {
|
||||
send_message(token, chat_id, "❌ " + t("mac_invalid"), "HTML");
|
||||
return view_devices(token, chat_id, null);
|
||||
}
|
||||
|
||||
let c = uci_core.cursor();
|
||||
if (!c) {
|
||||
return send_message(token, chat_id, "❌ " + t("mac_firewall_open_failed"), "HTML",
|
||||
[[{ text: "⬅️ " + t("nav_menu"), callback_data: "/menu" }]]);
|
||||
}
|
||||
c.load("firewall");
|
||||
|
||||
let existing = find_mac_block_rules(c, mac);
|
||||
let blocked_now;
|
||||
if (length(existing) > 0) {
|
||||
for (let name in existing)
|
||||
c.delete("firewall", name);
|
||||
blocked_now = false;
|
||||
} else {
|
||||
let sec = c.add("firewall", "rule");
|
||||
if (!sec) {
|
||||
return send_message(token, chat_id, "❌ " + t("mac_block_rule_failed"), "HTML",
|
||||
[[{ text: "⬅️ " + t("nav_menu"), callback_data: "/menu" }]]);
|
||||
}
|
||||
c.set("firewall", sec, "name", "Tachyon block " + mac);
|
||||
c.set("firewall", sec, "src", "lan");
|
||||
c.set("firewall", sec, "dest", "*");
|
||||
c.set("firewall", sec, "proto", "all");
|
||||
c.set("firewall", sec, "src_mac", mac);
|
||||
c.set("firewall", sec, "target", "REJECT");
|
||||
blocked_now = true;
|
||||
}
|
||||
c.commit("firewall");
|
||||
// Reload in the background so the poll loop is not blocked by fw4
|
||||
system(common.background_command("/etc/init.d/firewall reload"));
|
||||
|
||||
send_message(token, chat_id,
|
||||
(blocked_now ? "🚫 Устройство <code>" : "🔓 Устройство <code>") + mac +
|
||||
(blocked_now ? "</code> заблокировано." : "</code> разблокировано.") +
|
||||
"\n\n<i>Правила firewall применяются в фоне (несколько секунд).</i>", "HTML");
|
||||
return view_devices(token, chat_id, null);
|
||||
}
|
||||
|
||||
function view_watchdog(token, chat_id, msg_id) {
|
||||
let running = api.process_running_by_pidfile("/var/run/tachyon_watchdog.pid");
|
||||
let text = "🐕 <b>Watchdog Tachyon</b>\n\n" +
|
||||
|
||||
@@ -764,6 +764,11 @@ function action_state_from_dirs() {
|
||||
};
|
||||
}
|
||||
|
||||
function release_dir_lock(lock_dir) {
|
||||
remove_file(as_string(lock_dir) + "/pid");
|
||||
command_success_from_args([ "rmdir", lock_dir ]);
|
||||
}
|
||||
|
||||
function acquire_dir_lock(lock_dir) {
|
||||
lock_dir = as_string(lock_dir);
|
||||
let owner_pid = current_pid();
|
||||
@@ -799,11 +804,6 @@ function acquire_dir_lock(lock_dir) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function release_dir_lock(lock_dir) {
|
||||
remove_file(as_string(lock_dir) + "/pid");
|
||||
command_success_from_args([ "rmdir", lock_dir ]);
|
||||
}
|
||||
|
||||
function service_enabled() {
|
||||
return file_executable("/etc/rc.d/S99" + SERVICE_NAME);
|
||||
}
|
||||
|
||||
@@ -527,45 +527,6 @@ function watch_recovery(key, incident, reason) {
|
||||
}
|
||||
|
||||
// The 30s floor is the original restart debounce: procd emits several stop
|
||||
// events for one crash, and each would otherwise queue its own restart.
|
||||
function heal_singbox_stopped(ev) {
|
||||
let now = time();
|
||||
if (now - last_restart_time < 30) return;
|
||||
last_restart_time = now;
|
||||
|
||||
let cfg = settings();
|
||||
if (cfg.recovery_bypass == "1") return;
|
||||
if (check_auto_resume_pause()) return;
|
||||
if (controller.check_tachyon_cli_running()) return;
|
||||
|
||||
let list_update_pid = trim(fs.readfile("/var/run/tachyon_list_update.pid") || "");
|
||||
if (process_running(list_update_pid, "ucode")) return;
|
||||
|
||||
log_message("sing-box is stopped (" + as_string(ev.payload.reason || "health check") + "). Restarting Tachyon...", "warn");
|
||||
increment_reconnect_count();
|
||||
let tcfg = common.object_or_empty(uci_core.get_all(CONFIG_NAME, "telegram"));
|
||||
if (tcfg.notify_crash != "0") {
|
||||
send_telegram_notification("⚠️ *Watchdog:* sing-box остановлен. Перезапускаю службы Tachyon...");
|
||||
}
|
||||
// This healer sends its own notification above and deliberately did not file
|
||||
// an incident, so it does not start one now. It does register the watch: a
|
||||
// restart that never brings the proxy back is worth a `failed` report, and
|
||||
// that report is the only thing here that is new.
|
||||
//
|
||||
// Pinned to the light rung: sing-box is not running, so there is nothing to
|
||||
// escalate over. Starting the service is the whole repair, and tearing down
|
||||
// nftables and routing would not make a stopped process start any better.
|
||||
if (safe_proxy_restart("singbox_stopped", ESCALATION_LIGHT)) {
|
||||
// sing-box coming back is what the proxy and DNS probes are measuring,
|
||||
// so their failures during the restart are this repair's own doing.
|
||||
singbox_repair_until = time() + SUPPRESSION_DEADLINE;
|
||||
watch_recovery("proxy", {
|
||||
type: "singbox_stopped",
|
||||
description: "sing-box остановлен (" + as_string(ev.payload.reason || "health check") + ")",
|
||||
resolution: "Выполнен перезапуск служб Tachyon"
|
||||
}, "singbox_stopped");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ─── AI Watchdog Self-Healing Matrix ──────────────────────────────────────────
|
||||
@@ -1348,6 +1309,46 @@ function increment_reconnect_count() {
|
||||
fs.writefile(count_file, as_string(count) + "\n");
|
||||
}
|
||||
|
||||
// events for one crash, and each would otherwise queue its own restart.
|
||||
function heal_singbox_stopped(ev) {
|
||||
let now = time();
|
||||
if (now - last_restart_time < 30) return;
|
||||
last_restart_time = now;
|
||||
|
||||
let cfg = settings();
|
||||
if (cfg.recovery_bypass == "1") return;
|
||||
if (check_auto_resume_pause()) return;
|
||||
if (controller.check_tachyon_cli_running()) return;
|
||||
|
||||
let list_update_pid = trim(fs.readfile("/var/run/tachyon_list_update.pid") || "");
|
||||
if (process_running(list_update_pid, "ucode")) return;
|
||||
|
||||
log_message("sing-box is stopped (" + as_string(ev.payload.reason || "health check") + "). Restarting Tachyon...", "warn");
|
||||
increment_reconnect_count();
|
||||
let tcfg = common.object_or_empty(uci_core.get_all(CONFIG_NAME, "telegram"));
|
||||
if (tcfg.notify_crash != "0") {
|
||||
send_telegram_notification("⚠️ *Watchdog:* sing-box остановлен. Перезапускаю службы Tachyon...");
|
||||
}
|
||||
// This healer sends its own notification above and deliberately did not file
|
||||
// an incident, so it does not start one now. It does register the watch: a
|
||||
// restart that never brings the proxy back is worth a `failed` report, and
|
||||
// that report is the only thing here that is new.
|
||||
//
|
||||
// Pinned to the light rung: sing-box is not running, so there is nothing to
|
||||
// escalate over. Starting the service is the whole repair, and tearing down
|
||||
// nftables and routing would not make a stopped process start any better.
|
||||
if (safe_proxy_restart("singbox_stopped", ESCALATION_LIGHT)) {
|
||||
// sing-box coming back is what the proxy and DNS probes are measuring,
|
||||
// so their failures during the restart are this repair's own doing.
|
||||
singbox_repair_until = time() + SUPPRESSION_DEADLINE;
|
||||
watch_recovery("proxy", {
|
||||
type: "singbox_stopped",
|
||||
description: "sing-box остановлен (" + as_string(ev.payload.reason || "health check") + ")",
|
||||
resolution: "Выполнен перезапуск служб Tachyon"
|
||||
}, "singbox_stopped");
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Adaptive Intervals ───────────────────────────────────────────────────────
|
||||
|
||||
// ─── Graceful Degradation wrapper ─────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user