generator.uc: try/catch around hosts cache file read with guaranteed close hosts.uc: try/catch around parse_hosts_file with guaranteed close migration.uc: try/catch around remove_cache_path, checked unlink in remove_legacy runtime.uc: cleanup stale temp config files on init_config startup lifecycle.uc: log warning on nft table delete failure in stop_main
1504 lines
53 KiB
Ucode
1504 lines
53 KiB
Ucode
#!/usr/bin/env ucode
|
|
|
|
let fs = require("fs");
|
|
let constants = require("core.constants");
|
|
let uci_core = require("core.uci");
|
|
let common = require("core.common");
|
|
|
|
function as_string(value) {
|
|
return value == null ? "" : "" + value;
|
|
}
|
|
|
|
function constant_value(name, fallback) {
|
|
let value = constants[name];
|
|
return value == null ? as_string(fallback) : as_string(value);
|
|
}
|
|
|
|
const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || constant_value("TACHYON_CONFIG_NAME", "tachyon");
|
|
const CONFIG_FILE = getenv("TACHYON_CONFIG_FILE") || "/etc/config/" + CONFIG_NAME;
|
|
const LIB_DIR = getenv("TACHYON_LIB") || "/usr/lib/tachyon";
|
|
const BIN_PATH = getenv("TACHYON_BIN") || constant_value("TACHYON_BIN", "/usr/bin/tachyon");
|
|
const SERVICE_INIT = getenv("TACHYON_SERVICE_INIT") || constant_value("TACHYON_SERVICE_INIT", "/etc/init.d/tachyon");
|
|
const SERVICE_NAME = getenv("TACHYON_SERVICE_NAME") || constant_value("TACHYON_SERVICE_NAME", "tachyon");
|
|
const LUCI_VIEW_DIR = getenv("TACHYON_LUCI_VIEW_DIR") || constant_value("TACHYON_LUCI_VIEW_DIR", "/www/luci-static/resources/view/tachyon");
|
|
const LUCI_I18N_DOMAIN = getenv("TACHYON_LUCI_I18N_DOMAIN") || constant_value("TACHYON_LUCI_I18N_DOMAIN", "tachyon");
|
|
|
|
const RUNTIME_STATE_DIR = getenv("TACHYON_RUNTIME_STATE_DIR") || "/var/run/tachyon";
|
|
const SYSTEM_INFO_CACHE_FILE = getenv("TACHYON_SYSTEM_INFO_CACHE_FILE") || RUNTIME_STATE_DIR + "/system-info.json";
|
|
const RELOAD_STATE_FILE = getenv("TACHYON_RELOAD_STATE_FILE") || RUNTIME_STATE_DIR + "/reload-state";
|
|
const RELOAD_STATE_SNAPSHOT_FILE = getenv("TACHYON_RELOAD_STATE_SNAPSHOT_FILE") || RUNTIME_STATE_DIR + "/reload-state.snapshot." + clock()[0] + "." + clock()[1];
|
|
const PENDING_RELOAD_FILE = getenv("TACHYON_PENDING_RELOAD_FILE") || RUNTIME_STATE_DIR + "/reload.pending";
|
|
const SERVICE_TRIGGER_SYNC_FILE = getenv("TACHYON_SERVICE_TRIGGER_SYNC_FILE") || RUNTIME_STATE_DIR + "/service-triggers.sync";
|
|
const SUBSCRIPTION_UPDATE_STATE_DIR = getenv("TACHYON_SUBSCRIPTION_UPDATE_STATE_DIR") || RUNTIME_STATE_DIR + "/subscription-update";
|
|
const SUBSCRIPTION_LINKS_DIR = getenv("TACHYON_SUBSCRIPTION_LINKS_DIR") || RUNTIME_STATE_DIR + "/subscription-links";
|
|
const SUBSCRIPTION_METADATA_DIR = getenv("TACHYON_SUBSCRIPTION_METADATA_DIR") || RUNTIME_STATE_DIR + "/subscription-metadata";
|
|
const OUTBOUND_METADATA_DIR = getenv("TACHYON_OUTBOUND_METADATA_DIR") || RUNTIME_STATE_DIR + "/outbound-metadata";
|
|
const SECTION_CACHE_DIR = getenv("TACHYON_SECTION_CACHE_DIR") || RUNTIME_STATE_DIR + "/section-cache";
|
|
const RULE_CONDITION_CACHE_DIR = getenv("TACHYON_RULE_CONDITION_CACHE_DIR") || RUNTIME_STATE_DIR + "/rule-condition-cache";
|
|
const RUNTIME_CACHE_FORMAT_FILE = getenv("TACHYON_RUNTIME_CACHE_FORMAT_FILE") || RUNTIME_STATE_DIR + "/cache-format";
|
|
const PERSISTENT_SUBSCRIPTION_CACHE_DIR = getenv("TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_DIR") || "/etc/tachyon/subscription-cache";
|
|
const PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE = getenv("TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE") || PERSISTENT_SUBSCRIPTION_CACHE_DIR + "/cache-format";
|
|
const PERSISTENT_SUBSCRIPTION_CACHE_FORMAT = getenv("TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT") || "7";
|
|
const SUBSCRIPTION_BOOTSTRAP_RETRY_PID_FILE = getenv("TACHYON_SUBSCRIPTION_BOOTSTRAP_RETRY_PID_FILE") || RUNTIME_STATE_DIR + "/subscription-bootstrap-retry.pid";
|
|
const DNS_FAILOVER_STATE_FILE = getenv("TACHYON_DNS_FAILOVER_STATE_FILE") || RUNTIME_STATE_DIR + "/dns-failover.json";
|
|
const DNS_FAILOVER_PID_FILE = getenv("TACHYON_DNS_FAILOVER_PID_FILE") || RUNTIME_STATE_DIR + "/dns-failover.pid";
|
|
const SUBSCRIPTION_UPDATE_LOCK_DIR = getenv("TACHYON_SUBSCRIPTION_UPDATE_LOCK_DIR") || RUNTIME_STATE_DIR + "/subscription-update.lock";
|
|
const RELOAD_LOCK_DIR = getenv("TACHYON_RELOAD_LOCK_DIR") || "/var/run/tachyon.reload.lock";
|
|
const INTERNAL_CONFIG_TRIGGER_GUARD = getenv("TACHYON_INTERNAL_CONFIG_TRIGGER_GUARD") || "/var/run/tachyon.internal-config-change";
|
|
|
|
const LIST_UPDATE_CRON_MARKER = getenv("TACHYON_LIST_UPDATE_CRON_MARKER") || "# tachyon-list-update";
|
|
const SUBSCRIPTION_UPDATE_CRON_MARKER = getenv("TACHYON_SUBSCRIPTION_UPDATE_CRON_MARKER") || "# tachyon-subscription-update";
|
|
const COMPONENT_UPDATE_CHECK_CRON_MARKER = getenv("TACHYON_COMPONENT_UPDATE_CHECK_CRON_MARKER") || "# tachyon-component-update-check";
|
|
const RELOAD_STATE_FORMAT = int(getenv("TACHYON_RELOAD_STATE_FORMAT") || "1");
|
|
const RUNTIME_CACHE_FORMAT = int(getenv("TACHYON_RUNTIME_CACHE_FORMAT") || "8");
|
|
const RUNTIME_STABLE_MIN_AGE = int(getenv("TACHYON_RUNTIME_STABLE_MIN_AGE") || "2");
|
|
const SING_BOX_START_STABLE_MIN_AGE = int(getenv("TACHYON_SING_BOX_START_STABLE_MIN_AGE") || "8");
|
|
const SING_BOX_START_VERIFY_TIMEOUT = int(getenv("TACHYON_SING_BOX_START_VERIFY_TIMEOUT") || "45");
|
|
const NFT_POPULATE_ENABLED_DEFAULT = int(getenv("TACHYON_NFT_POPULATE_ENABLED") || "1");
|
|
|
|
const TMP_SING_BOX_FOLDER = getenv("TMP_SING_BOX_FOLDER") || constant_value("TMP_SING_BOX_FOLDER", "/tmp/sing-box");
|
|
const TMP_RULESET_FOLDER = getenv("TMP_RULESET_FOLDER") || constant_value("TMP_RULESET_FOLDER", TMP_SING_BOX_FOLDER + "/rulesets");
|
|
const TMP_SUBSCRIPTION_FOLDER = getenv("TMP_SUBSCRIPTION_FOLDER") || constant_value("TMP_SUBSCRIPTION_FOLDER", TMP_SING_BOX_FOLDER + "/subscriptions");
|
|
|
|
const RT_TABLE_NAME = constant_value("RT_TABLE_NAME", "tachyon");
|
|
const NFT_TABLE_NAME = constant_value("NFT_TABLE_NAME", "TachyonTable");
|
|
const NFT_LOCALV4_SET_NAME = constant_value("NFT_LOCALV4_SET_NAME", "localv4");
|
|
const NFT_LOCALV6_SET_NAME = constant_value("NFT_LOCALV6_SET_NAME", "localv6");
|
|
const NFT_COMMON_SET_NAME = constant_value("NFT_COMMON_SET_NAME", "tachyon_subnets");
|
|
const NFT_COMMON6_SET_NAME = constant_value("NFT_COMMON6_SET_NAME", "tachyon_subnets6");
|
|
const NFT_PORT_SET_NAME = constant_value("NFT_PORT_SET_NAME", "tachyon_ports");
|
|
const NFT_IP_PORT_SET_NAME = constant_value("NFT_IP_PORT_SET_NAME", "tachyon_ip_ports");
|
|
const NFT_IP_PORT6_SET_NAME = constant_value("NFT_IP_PORT6_SET_NAME", "tachyon_ip6_ports");
|
|
const NFT_INTERFACE_SET_NAME = constant_value("NFT_INTERFACE_SET_NAME", "tachyon_interfaces");
|
|
const NFT_FAKEIP_MARK = constant_value("NFT_FAKEIP_MARK", "0x04000000");
|
|
const NFT_OUTBOUND_MARK = constant_value("NFT_OUTBOUND_MARK", "0x08000000");
|
|
|
|
const SB_FAKEIP_INET4_RANGE = constant_value("SB_FAKEIP_INET4_RANGE", "198.18.0.0/15");
|
|
const SB_FAKEIP_INET6_RANGE = constant_value("SB_FAKEIP_INET6_RANGE", "fc00::/18");
|
|
const SB_TPROXY_INBOUND6_ADDRESS = constant_value("SB_TPROXY_INBOUND6_ADDRESS", "::1");
|
|
const SB_TPROXY_INBOUND_PORT = constant_value("SB_TPROXY_INBOUND_PORT", "1602");
|
|
const SB_SERVICE_MIXED_INBOUND_ADDRESS = constant_value("SB_SERVICE_MIXED_INBOUND_ADDRESS", "127.0.0.1");
|
|
const SB_SERVICE_MIXED_INBOUND_PORT = constant_value("SB_SERVICE_MIXED_INBOUND_PORT", "4534");
|
|
const SB_VARIANT_STATE_FILE = constant_value("SB_VARIANT_STATE_FILE", "/etc/tachyon/sing-box-variant");
|
|
const SB_VERSION_STATE_FILE = constant_value("SB_VERSION_STATE_FILE", "/etc/tachyon/sing-box-version");
|
|
|
|
const ZAPRET_PROVIDER_NFQWS_BIN = constant_value("ZAPRET_PROVIDER_NFQWS_BIN", "/opt/zapret/nfq/nfqws");
|
|
const ZAPRET_ROUTE_MARK_BASE = constant_value("ZAPRET_ROUTE_MARK_BASE", "0x01000000");
|
|
const ZAPRET_QUEUE_BASE = constant_value("ZAPRET_QUEUE_BASE", "4000");
|
|
const ZAPRET_DESYNC_MARK = constant_value("ZAPRET_DESYNC_MARK", "0x40000000");
|
|
const ZAPRET_DESYNC_MARK_POSTNAT = constant_value("ZAPRET_DESYNC_MARK_POSTNAT", "0x20000000");
|
|
const ZAPRET2_PROVIDER_NFQWS2_BIN = constant_value("ZAPRET2_PROVIDER_NFQWS2_BIN", "/opt/zapret2/nfq2/nfqws2");
|
|
const ZAPRET2_ROUTE_MARK_BASE = constant_value("ZAPRET2_ROUTE_MARK_BASE", "0x02000000");
|
|
const ZAPRET2_QUEUE_BASE = constant_value("ZAPRET2_QUEUE_BASE", "4300");
|
|
const ZAPRET2_DESYNC_MARK = constant_value("ZAPRET2_DESYNC_MARK", "0x40000000");
|
|
const ZAPRET2_DESYNC_MARK_POSTNAT = constant_value("ZAPRET2_DESYNC_MARK_POSTNAT", "0x20000000");
|
|
const BYEDPI_BIN = constant_value("BYEDPI_BIN", "/usr/bin/ciadpi");
|
|
|
|
const DNS_APPLY_UC = LIB_DIR + "/dns/apply.uc";
|
|
const VALIDATOR_UC = LIB_DIR + "/config/validator.uc";
|
|
const SERVER_UC = LIB_DIR + "/server/service.uc";
|
|
const NFT_UC = LIB_DIR + "/nft/apply.uc";
|
|
const SINGBOX_UC = LIB_DIR + "/singbox/runtime.uc";
|
|
const PRIORITY_UC = LIB_DIR + "/singbox/priority.uc";
|
|
const DNS_FAILOVER_UC = LIB_DIR + "/singbox/dns_failover.uc";
|
|
const SUBSCRIPTION_CACHE_UC = LIB_DIR + "/subscription/cache.uc";
|
|
const UPDATES_UC = LIB_DIR + "/components/updates.uc";
|
|
const STATE_UC = LIB_DIR + "/service/state.uc";
|
|
const RELOAD_UC = LIB_DIR + "/service/reload.uc";
|
|
const UI_UC = LIB_DIR + "/service/ui.uc";
|
|
const DIAGNOSTICS_UC = LIB_DIR + "/diagnostics/runtime.uc";
|
|
const ZAPRET_UC = LIB_DIR + "/providers/zapret/runtime.uc";
|
|
const ZAPRET2_UC = LIB_DIR + "/providers/zapret2/runtime.uc";
|
|
const BYEDPI_UC = LIB_DIR + "/providers/byedpi/runtime.uc";
|
|
const PACKAGES_UC = LIB_DIR + "/core/packages.uc";
|
|
const WATCHDOG_UC = LIB_DIR + "/service/watchdog.uc";
|
|
const TELEGRAM_UC = LIB_DIR + "/service/telegram.uc";
|
|
const DNS_PREFETCH_UC = LIB_DIR + "/singbox/dns_prefetch.uc";
|
|
|
|
let start_subscription_update_lock_held = false;
|
|
let subscription_caches_prepared = getenv("TACHYON_SUBSCRIPTION_CACHES_PREPARED") || "0";
|
|
let subscription_runtime_no_refresh = getenv("TACHYON_SUBSCRIPTION_RUNTIME_NO_REFRESH") || "0";
|
|
let subscription_deferred_sections = "";
|
|
let nft_populate_enabled = NFT_POPULATE_ENABLED_DEFAULT;
|
|
let rule_condition_cache_enabled = 0;
|
|
let startup_config_fingerprint = "";
|
|
|
|
function shell_quote(value) {
|
|
return "'" + replace(as_string(value), /'/g, "'\\''") + "'";
|
|
}
|
|
|
|
function command_from_args(args) {
|
|
let parts = [];
|
|
for (let arg in args)
|
|
push(parts, shell_quote(arg));
|
|
return join(" ", parts);
|
|
}
|
|
|
|
function normalize_status(status) {
|
|
status = int(status);
|
|
return status > 255 ? int(status / 256) : status;
|
|
}
|
|
|
|
function command_status(command) {
|
|
return normalize_status(system(command));
|
|
}
|
|
|
|
function command_capture(command) {
|
|
let pipe = fs.popen(command, "r");
|
|
if (!pipe)
|
|
return { status: 1, output: "" };
|
|
|
|
let data = pipe.read("all");
|
|
let status = normalize_status(pipe.close());
|
|
return { status, output: data == null ? "" : as_string(data) };
|
|
}
|
|
|
|
function command_output(command) {
|
|
let result = command_capture(command);
|
|
return result.status == 0 ? result.output : "";
|
|
}
|
|
|
|
function read_json_file(path) {
|
|
let data = fs.readfile(as_string(path));
|
|
if (data == null)
|
|
return null;
|
|
|
|
try {
|
|
return json(data);
|
|
}
|
|
catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function write_json(value) {
|
|
print(sprintf("%J", value), "\n");
|
|
}
|
|
|
|
function command_success(command) {
|
|
return command_status(command + " >/dev/null 2>&1") == 0;
|
|
}
|
|
|
|
function command_status_from_args(args) {
|
|
return command_status(command_from_args(args));
|
|
}
|
|
|
|
function command_output_from_args(args) {
|
|
return command_output(command_from_args(args) + " 2>/dev/null");
|
|
}
|
|
|
|
function command_success_from_args(args) {
|
|
return command_status(command_from_args(args) + " >/dev/null 2>&1") == 0;
|
|
}
|
|
|
|
function external_config_fingerprint() {
|
|
let data = fs.readfile(CONFIG_FILE);
|
|
if (data == null)
|
|
return "";
|
|
|
|
let lines = [];
|
|
for (let line in split(as_string(data), "\n"))
|
|
if (match(as_string(line), /^[ \t]*option[ \t]+shutdown_correctly([ \t]|$)/) == null)
|
|
push(lines, line);
|
|
|
|
return join("\n", lines);
|
|
}
|
|
|
|
function owner_pid() {
|
|
let status = fs.readfile("/proc/self/status") || "";
|
|
let m = match(status, /PPid:\s+([0-9]+)/);
|
|
return m ? m[1] : "0";
|
|
}
|
|
|
|
function bool_text(value) {
|
|
value = lc(as_string(value));
|
|
return value == "1" || value == "true" || value == "yes" || value == "on";
|
|
}
|
|
|
|
function object_or_empty(value) {
|
|
return type(value) == "object" ? value : {};
|
|
}
|
|
|
|
function array_or_empty(value) {
|
|
return type(value) == "array" ? value : [];
|
|
}
|
|
|
|
function string_array_contains(values, needle) {
|
|
needle = as_string(needle);
|
|
if (needle == "")
|
|
return false;
|
|
|
|
for (let item in array_or_empty(values))
|
|
if (as_string(item) == needle)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
function write_file(path, value) {
|
|
return fs.writefile(as_string(path), as_string(value));
|
|
}
|
|
|
|
function remove_file(path) {
|
|
return fs.unlink(as_string(path)) || true;
|
|
}
|
|
|
|
function ensure_dir(path) {
|
|
path = as_string(path);
|
|
return path == "" || fs.mkdir(path, 0755) || fs.stat(path) != null;
|
|
}
|
|
|
|
function log_message(message, level) {
|
|
level = as_string(level || "info");
|
|
command_success_from_args([ "logger", "-t", SERVICE_NAME, "[" + level + "] " + as_string(message) ]);
|
|
}
|
|
|
|
function lifecycle_env() {
|
|
return {
|
|
TACHYON_CONFIG_NAME: CONFIG_NAME,
|
|
TACHYON_LIB: LIB_DIR,
|
|
TACHYON_BIN: BIN_PATH,
|
|
TACHYON_SERVICE_INIT: SERVICE_INIT,
|
|
TACHYON_RUNTIME_STATE_DIR: RUNTIME_STATE_DIR,
|
|
TACHYON_SYSTEM_INFO_CACHE_FILE: SYSTEM_INFO_CACHE_FILE,
|
|
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_RULE_CONDITION_CACHE_DIR: RULE_CONDITION_CACHE_DIR,
|
|
TACHYON_RUNTIME_CACHE_FORMAT_FILE: RUNTIME_CACHE_FORMAT_FILE,
|
|
TACHYON_RUNTIME_CACHE_FORMAT: as_string(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_SUBSCRIPTION_BOOTSTRAP_RETRY_PID_FILE: SUBSCRIPTION_BOOTSTRAP_RETRY_PID_FILE,
|
|
TACHYON_DNS_FAILOVER_STATE_FILE: DNS_FAILOVER_STATE_FILE,
|
|
TACHYON_DNS_FAILOVER_PID_FILE: DNS_FAILOVER_PID_FILE,
|
|
TACHYON_SUBSCRIPTION_UPDATE_LOCK_DIR: SUBSCRIPTION_UPDATE_LOCK_DIR,
|
|
TACHYON_PENDING_RELOAD_FILE: PENDING_RELOAD_FILE,
|
|
TACHYON_RELOAD_LOCK_DIR: RELOAD_LOCK_DIR,
|
|
TMP_SING_BOX_FOLDER: TMP_SING_BOX_FOLDER,
|
|
TMP_RULESET_FOLDER: TMP_RULESET_FOLDER,
|
|
TMP_SUBSCRIPTION_FOLDER: TMP_SUBSCRIPTION_FOLDER,
|
|
SB_SERVICE_MIXED_INBOUND_ADDRESS: SB_SERVICE_MIXED_INBOUND_ADDRESS,
|
|
SB_SERVICE_MIXED_INBOUND_PORT: SB_SERVICE_MIXED_INBOUND_PORT,
|
|
SB_VARIANT_STATE_FILE: SB_VARIANT_STATE_FILE,
|
|
SB_VERSION_STATE_FILE: SB_VERSION_STATE_FILE,
|
|
ZAPRET_PROVIDER_NFQWS_BIN: ZAPRET_PROVIDER_NFQWS_BIN,
|
|
ZAPRET2_PROVIDER_NFQWS2_BIN: ZAPRET2_PROVIDER_NFQWS2_BIN,
|
|
BYEDPI_BIN: BYEDPI_BIN,
|
|
TACHYON_RULE_CONDITION_CACHE_ENABLED: as_string(rule_condition_cache_enabled)
|
|
};
|
|
}
|
|
|
|
function command_env(assignments) {
|
|
let parts = [];
|
|
for (let name, value in assignments)
|
|
push(parts, as_string(name) + "=" + shell_quote(value));
|
|
return join(" ", parts);
|
|
}
|
|
|
|
function module_args(module_path, args) {
|
|
let result = [ "ucode", "-L", LIB_DIR, module_path ];
|
|
for (let arg in (type(args) == "array" ? args : []))
|
|
push(result, arg);
|
|
return result;
|
|
}
|
|
|
|
function module_command(module_path, args) {
|
|
return command_env(lifecycle_env()) + " " + command_from_args(module_args(module_path, args));
|
|
}
|
|
|
|
function module_capture(module_path, args) {
|
|
return command_capture(module_command(module_path, args));
|
|
}
|
|
|
|
function module_status(module_path, args) {
|
|
return command_status(module_command(module_path, args));
|
|
}
|
|
|
|
function module_success(module_path, args) {
|
|
return module_status(module_path, args) == 0;
|
|
}
|
|
|
|
function mark_pending_reload(reason) {
|
|
return module_success(STATE_UC, [ "mark-pending-reload", PENDING_RELOAD_FILE, reason ]);
|
|
}
|
|
|
|
function pending_reload_log_context(reason) {
|
|
reason = as_string(reason);
|
|
if (reason == "config_changed_during_reload")
|
|
return "current reload";
|
|
return "startup";
|
|
}
|
|
|
|
function mark_pending_reload_if_config_changed(initial_fingerprint, reason) {
|
|
initial_fingerprint = as_string(initial_fingerprint);
|
|
if (initial_fingerprint == "")
|
|
return false;
|
|
|
|
let current_fingerprint = external_config_fingerprint();
|
|
if (current_fingerprint == "" || current_fingerprint == initial_fingerprint)
|
|
return false;
|
|
|
|
let context = pending_reload_log_context(reason);
|
|
if (mark_pending_reload(reason)) {
|
|
log_message("Configuration changed during " + context + "; queued reload after " + context + " completes", "info");
|
|
return true;
|
|
}
|
|
|
|
log_message("Configuration changed during " + context + ", but pending reload could not be queued", "warn");
|
|
return false;
|
|
}
|
|
|
|
function finish_reload_status(status, initial_fingerprint) {
|
|
status = int(status || 0);
|
|
if (status == 0)
|
|
mark_pending_reload_if_config_changed(initial_fingerprint, "config_changed_during_reload");
|
|
return status;
|
|
}
|
|
|
|
function module_output(module_path, args) {
|
|
let result = module_capture(module_path, args);
|
|
return result.status == 0 ? result.output : "";
|
|
}
|
|
|
|
function selector_state_from_proxies_payload(payload) {
|
|
let result = {};
|
|
let proxies = object_or_empty(object_or_empty(payload).proxies);
|
|
|
|
for (let tag, proxy in proxies) {
|
|
proxy = object_or_empty(proxy);
|
|
let proxy_type = lc(as_string(proxy.type || ""));
|
|
let selected = as_string(proxy.now || "");
|
|
|
|
if (proxy_type == "selector" && string_array_contains(proxy.all, selected))
|
|
result[as_string(tag)] = selected;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function selector_restore_pairs(snapshot, payload) {
|
|
let result = [];
|
|
snapshot = object_or_empty(snapshot);
|
|
let proxies = object_or_empty(object_or_empty(payload).proxies);
|
|
|
|
for (let group, selected in snapshot) {
|
|
group = as_string(group);
|
|
selected = as_string(selected);
|
|
|
|
if (group == "" || selected == "")
|
|
continue;
|
|
|
|
let proxy = object_or_empty(proxies[group]);
|
|
if (lc(as_string(proxy.type || "")) != "selector")
|
|
continue;
|
|
if (!string_array_contains(proxy.all, selected))
|
|
continue;
|
|
if (as_string(proxy.now || "") == selected)
|
|
continue;
|
|
|
|
push(result, { group, proxy: selected });
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function clash_api_json(action, arg1, arg2, arg3) {
|
|
let result = module_capture(DIAGNOSTICS_UC, [
|
|
"clash-api",
|
|
action,
|
|
as_string(arg1 || ""),
|
|
as_string(arg2 || ""),
|
|
as_string(arg3 || "")
|
|
]);
|
|
if (result.status != 0)
|
|
return null;
|
|
|
|
try {
|
|
return json(result.output);
|
|
}
|
|
catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function capture_selector_state() {
|
|
return selector_state_from_proxies_payload(clash_api_json("get_proxies"));
|
|
}
|
|
|
|
function restore_selector_state(snapshot) {
|
|
let pairs = selector_restore_pairs(snapshot, clash_api_json("get_proxies"));
|
|
|
|
for (let pair in pairs)
|
|
module_success(DIAGNOSTICS_UC, [ "clash-api", "set_group_proxy", pair.group, pair.proxy, "" ]);
|
|
}
|
|
|
|
function module_background(module_path, args) {
|
|
return command_status(module_command(module_path, args) + " >/dev/null 2>&1 1000>&- &") == 0;
|
|
}
|
|
|
|
function config_get(path, fallback) {
|
|
path = as_string(path);
|
|
if (!uci_core.exists(path))
|
|
return as_string(fallback);
|
|
return trim(uci_core.get(path));
|
|
}
|
|
|
|
function config_set(path, value) {
|
|
return uci_core.set(path, value);
|
|
}
|
|
|
|
function file_md5(path) {
|
|
path = as_string(path);
|
|
if (path == "" || fs.stat(path) == null)
|
|
return "";
|
|
|
|
let output = command_output("md5sum " + shell_quote(path) + " 2>/dev/null");
|
|
let fields = split(trim(output), /[ \t\r\n]+/);
|
|
return length(fields) > 0 ? as_string(fields[0]) : "";
|
|
}
|
|
|
|
function current_config_hash() {
|
|
return file_md5(CONFIG_FILE);
|
|
}
|
|
|
|
function mark_internal_config_guard() {
|
|
let hash = current_config_hash();
|
|
if (hash == "") {
|
|
fs.unlink(INTERNAL_CONFIG_TRIGGER_GUARD);
|
|
return;
|
|
}
|
|
|
|
let stamp = clock();
|
|
let tmp_path = INTERNAL_CONFIG_TRIGGER_GUARD + "." + stamp[0] + "." + stamp[1];
|
|
fs.writefile(tmp_path, as_string(stamp[0]) + "\n" + hash + "\n");
|
|
if (!fs.rename(tmp_path, INTERNAL_CONFIG_TRIGGER_GUARD))
|
|
fs.unlink(tmp_path);
|
|
}
|
|
|
|
function config_commit() {
|
|
if (!uci_core.commit(CONFIG_NAME))
|
|
return 1;
|
|
mark_internal_config_guard();
|
|
return 0;
|
|
}
|
|
|
|
function mark_runtime_stopped_clean() {
|
|
let status = 0;
|
|
if (!config_set(CONFIG_NAME + ".settings.shutdown_correctly", "1"))
|
|
status = 1;
|
|
else {
|
|
let commit_status = config_commit();
|
|
if (commit_status != 0)
|
|
status = commit_status;
|
|
}
|
|
|
|
module_success(STATE_UC, [ "clear-reload-state", RELOAD_STATE_FILE, RELOAD_STATE_SNAPSHOT_FILE ]);
|
|
return status;
|
|
}
|
|
|
|
function setting_bool(name, fallback) {
|
|
let value = config_get(CONFIG_NAME + ".settings." + as_string(name), fallback ? "1" : "0");
|
|
return bool_text(value);
|
|
}
|
|
|
|
function dns_apply_status(args) {
|
|
return module_status(DNS_APPLY_UC, args);
|
|
}
|
|
|
|
function dns_apply_success(args) {
|
|
return dns_apply_status(args) == 0;
|
|
}
|
|
|
|
function dnsmasq_configure(force) {
|
|
let args = [ "configure" ];
|
|
if (force)
|
|
push(args, "force");
|
|
return dns_apply_status(args);
|
|
}
|
|
|
|
function dnsmasq_restore(force) {
|
|
let args = [ "restore" ];
|
|
if (force)
|
|
push(args, "force");
|
|
return dns_apply_status(args);
|
|
}
|
|
|
|
function dnsmasq_restore_fail_safe() {
|
|
return dns_apply_status([ "failsafe-restore" ]);
|
|
}
|
|
|
|
function dnsmasq_has_tachyon_managed_state() {
|
|
return dns_apply_success([ "has-managed-state" ]);
|
|
}
|
|
|
|
function discover_awg_mtu() {
|
|
let sections = uci_core.get_all(CONFIG_NAME, "outbound") || {};
|
|
let changed = false;
|
|
for (let section in sections) {
|
|
if (section.action == "awg" && (section.awg_mtu == "" || section.awg_mtu == "0" || section.awg_mtu == null)) {
|
|
let endpoint = section.awg_server_address;
|
|
if (endpoint) {
|
|
log_message("Auto-discovering optimal MTU for AWG section '" + section[".name"] + "'...", "info");
|
|
let best_mtu = 1500;
|
|
let test_mtus = [1500, 1400, 1280, 1200];
|
|
for (let test_mtu in test_mtus) {
|
|
let payload = test_mtu - 28;
|
|
let result = command_status(command_from_args(["ping", "-c", "1", "-W", "1", "-M", "do", "-s", as_string(payload), endpoint]));
|
|
if (result == 0) {
|
|
best_mtu = test_mtu;
|
|
break;
|
|
}
|
|
}
|
|
log_message(sprintf("Discovered optimal MTU %d for %s", best_mtu, section[".name"]), "info");
|
|
command_status(command_from_args(["uci", "set", "tachyon." + section[".name"] + ".awg_mtu=" + as_string(best_mtu)]));
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
if (changed) {
|
|
if (!command_success_from_args(["uci", "commit", "tachyon"]))
|
|
log_message("AWG MTU: uci commit tachyon failed", "warn");
|
|
}
|
|
}
|
|
|
|
function validate_start_config() {
|
|
let status = module_status(VALIDATOR_UC, [ "check-requirements" ]);
|
|
if (status != 0)
|
|
return status;
|
|
|
|
status = module_status(SERVER_UC, [ "prepare-all-defaults" ]);
|
|
if (status != 0)
|
|
return status;
|
|
|
|
status = module_status(VALIDATOR_UC, [ "validate-runtime" ]);
|
|
if (status != 0) {
|
|
log_message("Runtime config validation failed. Aborted.", "fatal");
|
|
return status;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
function acquire_start_subscription_update_lock() {
|
|
if (module_success(STATE_UC, [ "acquire-runtime-dir-lock-wait", SUBSCRIPTION_UPDATE_LOCK_DIR, owner_pid(), "300" ])) {
|
|
start_subscription_update_lock_held = true;
|
|
return true;
|
|
}
|
|
|
|
log_message("Subscription update is already running during startup. Aborted.", "fatal");
|
|
return false;
|
|
}
|
|
|
|
function release_start_subscription_update_lock() {
|
|
if (!start_subscription_update_lock_held)
|
|
return;
|
|
|
|
module_success(STATE_UC, [ "release-runtime-dir-lock", SUBSCRIPTION_UPDATE_LOCK_DIR ]);
|
|
start_subscription_update_lock_held = false;
|
|
}
|
|
|
|
function nft_rebuild_runtime() {
|
|
return module_status(NFT_UC, [
|
|
"nft-rebuild-runtime-from-uci",
|
|
RT_TABLE_NAME,
|
|
NFT_TABLE_NAME,
|
|
NFT_LOCALV4_SET_NAME,
|
|
NFT_COMMON_SET_NAME,
|
|
NFT_PORT_SET_NAME,
|
|
NFT_IP_PORT_SET_NAME,
|
|
NFT_INTERFACE_SET_NAME,
|
|
NFT_FAKEIP_MARK,
|
|
NFT_OUTBOUND_MARK,
|
|
SB_FAKEIP_INET4_RANGE,
|
|
SB_TPROXY_INBOUND_PORT,
|
|
ZAPRET_PROVIDER_NFQWS_BIN,
|
|
ZAPRET_ROUTE_MARK_BASE,
|
|
ZAPRET_QUEUE_BASE,
|
|
ZAPRET_DESYNC_MARK,
|
|
ZAPRET_DESYNC_MARK_POSTNAT,
|
|
ZAPRET2_PROVIDER_NFQWS2_BIN,
|
|
ZAPRET2_ROUTE_MARK_BASE,
|
|
ZAPRET2_QUEUE_BASE,
|
|
ZAPRET2_DESYNC_MARK,
|
|
ZAPRET2_DESYNC_MARK_POSTNAT,
|
|
NFT_LOCALV6_SET_NAME,
|
|
NFT_COMMON6_SET_NAME,
|
|
NFT_IP_PORT6_SET_NAME,
|
|
SB_FAKEIP_INET6_RANGE,
|
|
SB_TPROXY_INBOUND6_ADDRESS
|
|
]);
|
|
}
|
|
|
|
function nft_populate_runtime_sets() {
|
|
return module_status(NFT_UC, [
|
|
"nft-populate-runtime-sets-from-uci",
|
|
as_string(nft_populate_enabled),
|
|
subscription_deferred_sections,
|
|
NFT_TABLE_NAME,
|
|
NFT_COMMON_SET_NAME,
|
|
NFT_PORT_SET_NAME,
|
|
NFT_IP_PORT_SET_NAME,
|
|
NFT_INTERFACE_SET_NAME,
|
|
NFT_LOCALV4_SET_NAME,
|
|
NFT_FAKEIP_MARK,
|
|
NFT_COMMON6_SET_NAME,
|
|
NFT_IP_PORT6_SET_NAME,
|
|
NFT_LOCALV6_SET_NAME
|
|
]);
|
|
}
|
|
|
|
function reload_firewall() {
|
|
let status = nft_rebuild_runtime();
|
|
if (status != 0)
|
|
return status;
|
|
|
|
return nft_populate_runtime_sets();
|
|
}
|
|
|
|
function singbox_init_config() {
|
|
let result = module_capture(SINGBOX_UC, [
|
|
"init-config",
|
|
as_string(nft_populate_enabled),
|
|
subscription_caches_prepared,
|
|
subscription_runtime_no_refresh
|
|
]);
|
|
if (result.status == 0) {
|
|
subscription_deferred_sections = trim(result.output);
|
|
subscription_caches_prepared = "1";
|
|
}
|
|
return result.status;
|
|
}
|
|
|
|
function refresh_cron() {
|
|
return module_status(UPDATES_UC, [
|
|
"refresh-cron-from-uci",
|
|
BIN_PATH,
|
|
LIST_UPDATE_CRON_MARKER,
|
|
SUBSCRIPTION_UPDATE_CRON_MARKER,
|
|
COMPONENT_UPDATE_CHECK_CRON_MARKER
|
|
]);
|
|
}
|
|
|
|
function remove_cron_jobs() {
|
|
return module_status(UPDATES_UC, [
|
|
"remove-cron-jobs",
|
|
LIST_UPDATE_CRON_MARKER,
|
|
SUBSCRIPTION_UPDATE_CRON_MARKER,
|
|
COMPONENT_UPDATE_CHECK_CRON_MARKER
|
|
]);
|
|
}
|
|
|
|
function prepare_subscription_caches(mode) {
|
|
let result = module_capture(SUBSCRIPTION_CACHE_UC, [
|
|
"prepare-caches",
|
|
mode,
|
|
subscription_caches_prepared,
|
|
subscription_runtime_no_refresh
|
|
]);
|
|
if (result.status == 0) {
|
|
subscription_deferred_sections = trim(result.output);
|
|
subscription_caches_prepared = "1";
|
|
}
|
|
return result.status;
|
|
}
|
|
|
|
function start_main() {
|
|
let status;
|
|
|
|
log_message("Starting Tachyon", "info");
|
|
|
|
discover_awg_mtu();
|
|
|
|
status = validate_start_config();
|
|
if (status != 0)
|
|
return status;
|
|
|
|
startup_config_fingerprint = external_config_fingerprint();
|
|
|
|
status = module_status(NFT_UC, [ "ensure-bridge-netfilter-disabled" ]);
|
|
if (status != 0)
|
|
return status;
|
|
|
|
module_success(STATE_UC, [ "sync-time-if-needed" ]);
|
|
|
|
status = module_status(SUBSCRIPTION_CACHE_UC, [ "ensure-runtime-dirs" ]);
|
|
if (status != 0)
|
|
return status;
|
|
|
|
if (!acquire_start_subscription_update_lock())
|
|
return 1;
|
|
|
|
status = prepare_subscription_caches("startup");
|
|
if (status != 0) {
|
|
log_message("Subscription caches are not ready. Aborted.", "fatal");
|
|
return status;
|
|
}
|
|
|
|
status = nft_rebuild_runtime();
|
|
if (status != 0)
|
|
return status;
|
|
|
|
status = module_status(SINGBOX_UC, [ "configure-service" ]);
|
|
if (status != 0)
|
|
return status;
|
|
|
|
status = singbox_init_config();
|
|
if (status != 0)
|
|
return status;
|
|
|
|
status = refresh_cron();
|
|
if (status != 0)
|
|
return status;
|
|
|
|
module_success(BYEDPI_UC, [ "start-runtime" ]);
|
|
|
|
if (!command_success_from_args([ "/etc/init.d/sing-box", "start" ])) {
|
|
log_message("Failed to start sing-box. Aborted.", "fatal");
|
|
return 1;
|
|
}
|
|
|
|
status = module_status(STATE_UC, [
|
|
"wait-tachyon-stable-start",
|
|
RT_TABLE_NAME,
|
|
NFT_TABLE_NAME,
|
|
NFT_FAKEIP_MARK,
|
|
as_string(SING_BOX_START_STABLE_MIN_AGE),
|
|
as_string(SING_BOX_START_VERIFY_TIMEOUT)
|
|
]);
|
|
if (status != 0) {
|
|
log_message("Startup verification failed after Tachyon was started; rolling back DNS changes", "warn");
|
|
return status;
|
|
}
|
|
|
|
status = module_status(PRIORITY_UC, [ "start-runtime" ]);
|
|
if (status != 0) {
|
|
log_message("Failed to start Priority runtime. Aborted.", "fatal");
|
|
return status;
|
|
}
|
|
|
|
status = module_status(SUBSCRIPTION_CACHE_UC, [ "run-deferred-bootstrap", subscription_deferred_sections ]);
|
|
if (status != 0)
|
|
return status;
|
|
|
|
release_start_subscription_update_lock();
|
|
module_success(ZAPRET_UC, [ "start-runtime" ]);
|
|
module_success(ZAPRET2_UC, [ "start-runtime" ]);
|
|
|
|
module_background(UPDATES_UC, [ "list-update" ]);
|
|
module_background(DNS_PREFETCH_UC, [ "prefetch" ]);
|
|
return 0;
|
|
}
|
|
|
|
function start_impl() {
|
|
// Start Watchdog & Telegram Bot early so they remain active even if proxy configuration fails
|
|
let wd_status = module_status(WATCHDOG_UC, [ "start-runtime" ]);
|
|
if (wd_status != 0)
|
|
log_message("Watchdog start-runtime failed (status " + as_string(wd_status) + ")", "warn");
|
|
let tg_status = module_status(TELEGRAM_UC, [ "start-runtime" ]);
|
|
if (tg_status != 0)
|
|
log_message("Telegram start-runtime failed (status " + as_string(tg_status) + ")", "warn");
|
|
|
|
let status = start_main();
|
|
if (status != 0)
|
|
return status;
|
|
|
|
if (!setting_bool("dont_touch_dhcp", false)) {
|
|
status = dnsmasq_configure(false);
|
|
if (status != 0)
|
|
return status;
|
|
}
|
|
else if (dnsmasq_has_tachyon_managed_state()) {
|
|
status = dnsmasq_restore(true);
|
|
if (status != 0)
|
|
return status;
|
|
}
|
|
|
|
if (!config_set(CONFIG_NAME + ".settings.shutdown_correctly", "0"))
|
|
return 1;
|
|
|
|
status = config_commit();
|
|
if (status != 0)
|
|
return status;
|
|
|
|
status = module_status(STATE_UC, [
|
|
"write-current-reload-state-clean",
|
|
RELOAD_STATE_FILE,
|
|
as_string(RELOAD_STATE_FORMAT),
|
|
RULE_CONDITION_CACHE_DIR
|
|
]);
|
|
if (status != 0)
|
|
return status;
|
|
|
|
status = module_status(DNS_FAILOVER_UC, [ "start-runtime" ]);
|
|
if (status != 0) {
|
|
log_message("Failed to start DNS failover runtime", "fatal");
|
|
return status;
|
|
}
|
|
|
|
module_background(DIAGNOSTICS_UC, [ "get-system-info" ]);
|
|
return 0;
|
|
}
|
|
|
|
function stop_main() {
|
|
let status = 0;
|
|
|
|
log_message("Stopping Tachyon", "info");
|
|
module_success(DNS_FAILOVER_UC, [ "stop-runtime" ]);
|
|
module_success(PRIORITY_UC, [ "stop-runtime" ]);
|
|
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 (command_success_from_args([ "nft", "list", "table", "inet", NFT_TABLE_NAME ])) {
|
|
if (!command_success_from_args([ "nft", "delete", "table", "inet", NFT_TABLE_NAME ]))
|
|
log_message("Failed to delete nft table " + NFT_TABLE_NAME, "warn");
|
|
}
|
|
|
|
if (module_success(NFT_UC, [ "tproxy-marking-rule4-present", RT_TABLE_NAME, NFT_FAKEIP_MARK ]))
|
|
command_success_from_args([ "ip", "-4", "rule", "del", "fwmark", NFT_FAKEIP_MARK + "/" + NFT_FAKEIP_MARK, "table", RT_TABLE_NAME, "priority", "105" ]);
|
|
if (module_success(NFT_UC, [ "tproxy-marking-rule6-present", RT_TABLE_NAME, NFT_FAKEIP_MARK ]))
|
|
command_success_from_args([ "ip", "-6", "rule", "del", "fwmark", NFT_FAKEIP_MARK + "/" + NFT_FAKEIP_MARK, "table", RT_TABLE_NAME, "priority", "105" ]);
|
|
|
|
if (module_success(NFT_UC, [ "tproxy-route4-present", RT_TABLE_NAME ]))
|
|
command_success_from_args([ "ip", "route", "flush", "table", RT_TABLE_NAME ]);
|
|
if (module_success(NFT_UC, [ "tproxy-route6-present", RT_TABLE_NAME ]))
|
|
command_success_from_args([ "ip", "-6", "route", "flush", "table", RT_TABLE_NAME ]);
|
|
|
|
let cache_path = uci_core.get(CONFIG_NAME + ".settings.cache_path") || "/tmp/sing-box/cache.db";
|
|
remove_file(cache_path);
|
|
|
|
let sing_box_status = command_status_from_args([ "/etc/init.d/sing-box", "stop" ]);
|
|
command_success_from_args([ "killall", "-q", "-9", "sing-box" ]);
|
|
if (sing_box_status != 0)
|
|
status = sing_box_status;
|
|
|
|
return status;
|
|
}
|
|
|
|
function cleanup_failed_runtime() {
|
|
let status = 0;
|
|
|
|
log_message("Cleaning up Tachyon runtime after failed start/reload", "info");
|
|
|
|
let stop_status = stop_main();
|
|
if (stop_status != 0)
|
|
status = stop_status;
|
|
|
|
let dns_status = dnsmasq_restore_fail_safe();
|
|
if (dns_status != 0 && status == 0)
|
|
status = dns_status;
|
|
|
|
let mark_status = mark_runtime_stopped_clean();
|
|
if (mark_status != 0 && status == 0)
|
|
status = mark_status;
|
|
|
|
if (status != 0)
|
|
log_message("Failed to fully clean up Tachyon runtime after start/reload failure", "warn");
|
|
|
|
return status;
|
|
}
|
|
|
|
function abort_reload(status, runtime_changed) {
|
|
status = int(status || 0);
|
|
if (status == 0)
|
|
status = 1;
|
|
|
|
if (runtime_changed)
|
|
cleanup_failed_runtime();
|
|
else
|
|
remove_file(RELOAD_STATE_SNAPSHOT_FILE);
|
|
|
|
return status;
|
|
}
|
|
|
|
function start() {
|
|
let status = start_impl();
|
|
release_start_subscription_update_lock();
|
|
|
|
if (status != 0) {
|
|
cleanup_failed_runtime();
|
|
return status;
|
|
}
|
|
|
|
mark_pending_reload_if_config_changed(startup_config_fingerprint, "config_changed_during_start");
|
|
|
|
status = module_status(STATE_UC, [
|
|
"wait-sing-box-service-stable",
|
|
as_string(SING_BOX_START_STABLE_MIN_AGE),
|
|
as_string(SING_BOX_START_VERIFY_TIMEOUT)
|
|
]);
|
|
if (status != 0) {
|
|
log_message("Startup verification failed after Tachyon was started; rolling back DNS changes", "warn");
|
|
cleanup_failed_runtime();
|
|
return status;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
function stop_impl() {
|
|
let status = 0;
|
|
|
|
if (!setting_bool("dont_touch_dhcp", false)) {
|
|
let dns_status = dnsmasq_restore(false);
|
|
if (dns_status != 0)
|
|
status = dns_status;
|
|
}
|
|
else if (dnsmasq_has_tachyon_managed_state()) {
|
|
let dns_status = dnsmasq_restore(true);
|
|
if (dns_status != 0)
|
|
status = dns_status;
|
|
}
|
|
|
|
let runtime_status = stop_main();
|
|
if (runtime_status != 0)
|
|
status = runtime_status;
|
|
|
|
if (!config_set(CONFIG_NAME + ".settings.shutdown_correctly", "1"))
|
|
status = 1;
|
|
else {
|
|
let commit_status = config_commit();
|
|
if (commit_status != 0)
|
|
status = commit_status;
|
|
}
|
|
|
|
module_success(STATE_UC, [ "clear-reload-state", RELOAD_STATE_FILE, RELOAD_STATE_SNAPSHOT_FILE ]);
|
|
|
|
if (status != 0)
|
|
dnsmasq_restore_fail_safe();
|
|
|
|
return status;
|
|
}
|
|
|
|
function stop() {
|
|
return stop_impl();
|
|
}
|
|
|
|
function restart_runtime_for_reload() {
|
|
let status;
|
|
let selector_state = capture_selector_state();
|
|
|
|
log_message("Reload requires a full Tachyon runtime restart", "info");
|
|
|
|
status = stop_main();
|
|
if (status != 0)
|
|
return status;
|
|
|
|
status = start_impl();
|
|
if (status != 0) {
|
|
cleanup_failed_runtime();
|
|
return status;
|
|
}
|
|
|
|
status = module_status(STATE_UC, [
|
|
"wait-sing-box-service-stable",
|
|
as_string(SING_BOX_START_STABLE_MIN_AGE),
|
|
as_string(SING_BOX_START_VERIFY_TIMEOUT)
|
|
]);
|
|
if (status != 0) {
|
|
log_message("Reload runtime restart verification failed after Tachyon was started; rolling back DNS changes", "fatal");
|
|
cleanup_failed_runtime();
|
|
return status;
|
|
}
|
|
|
|
restore_selector_state(selector_state);
|
|
remove_file(RELOAD_STATE_SNAPSHOT_FILE);
|
|
return 0;
|
|
}
|
|
|
|
function write_service_trigger_sync_state(changed) {
|
|
ensure_dir(RUNTIME_STATE_DIR);
|
|
write_file(SERVICE_TRIGGER_SYNC_FILE, as_string(changed) + "\n");
|
|
}
|
|
|
|
function parse_reload_plan(output) {
|
|
let result = {
|
|
changed_service_triggers: 0,
|
|
changed_dnsmasq: 0,
|
|
changed_sing_box: 0,
|
|
changed_nft: 0,
|
|
changed_zapret_queue: 0,
|
|
changed_zapret_runtime: 0,
|
|
changed_zapret2_queue: 0,
|
|
changed_zapret2_runtime: 0,
|
|
changed_byedpi_runtime: 0,
|
|
changed_cron: 0,
|
|
changed_list: 0,
|
|
needs_sing_box_reload: 0,
|
|
needs_nft_rebuild: 0,
|
|
needs_zapret_restart: 0,
|
|
needs_zapret2_restart: 0,
|
|
needs_byedpi_restart: 0,
|
|
needs_dnsmasq_configure: 0,
|
|
needs_dnsmasq_restore: 0,
|
|
needs_cron_refresh: 0,
|
|
needs_list_update: 0,
|
|
has_work: 0
|
|
};
|
|
|
|
for (let line in split(as_string(output), "\n")) {
|
|
line = as_string(line);
|
|
if (line == "")
|
|
continue;
|
|
let fields = split(line, "\t");
|
|
if (length(fields) < 2)
|
|
continue;
|
|
let key = fields[0];
|
|
if (result[key] != null)
|
|
result[key] = int(fields[1] || 0);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function append_reload_action(actions, enabled, label) {
|
|
if (!(enabled === true || int(enabled || 0) == 1))
|
|
return actions;
|
|
return actions + (actions != "" ? ", " : "") + label;
|
|
}
|
|
|
|
function reload_actions_summary(plan) {
|
|
let actions = "";
|
|
actions = append_reload_action(actions, plan.needs_sing_box_reload, "sing-box");
|
|
actions = append_reload_action(actions, plan.needs_nft_rebuild, "nftables");
|
|
actions = append_reload_action(actions, plan.needs_zapret_restart, "Zapret");
|
|
actions = append_reload_action(actions, plan.needs_zapret2_restart, "Zapret2");
|
|
actions = append_reload_action(actions, plan.needs_byedpi_restart, "ByeDPI");
|
|
actions = append_reload_action(actions, plan.needs_dnsmasq_configure || plan.needs_dnsmasq_restore, "dnsmasq");
|
|
actions = append_reload_action(actions, plan.needs_cron_refresh, "scheduled jobs");
|
|
actions = append_reload_action(actions, plan.needs_list_update, "remote lists");
|
|
return actions;
|
|
}
|
|
|
|
function release_reload_lock() {
|
|
module_success(STATE_UC, [ "release-runtime-dir-lock", RELOAD_LOCK_DIR ]);
|
|
}
|
|
|
|
function sing_box_runtime_pid() {
|
|
let result = module_capture(STATE_UC, [ "sing-box-service-runtime-pid" ]);
|
|
return result.status == 0 ? trim(result.output) : "";
|
|
}
|
|
|
|
function wait_dns_failover_state(candidate_state_path, attempts) {
|
|
attempts = int(attempts || 1);
|
|
for (let i = 0; i < attempts; i++) {
|
|
let status = module_status(DNS_FAILOVER_UC, [ "verify-state", candidate_state_path ]);
|
|
if (status == 0)
|
|
return 0;
|
|
if (i + 1 < attempts)
|
|
command_success_from_args([ "sleep", "1" ]);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
function dns_failover_apply(candidate_state_path) {
|
|
candidate_state_path = as_string(candidate_state_path);
|
|
if (candidate_state_path == "" || fs.stat(candidate_state_path) == null)
|
|
return 1;
|
|
|
|
if (!module_success(STATE_UC, [ "acquire-runtime-dir-lock-wait", RELOAD_LOCK_DIR, owner_pid(), "2" ]))
|
|
return 2;
|
|
|
|
let patch_result = module_capture(SINGBOX_UC, [ "patch-dns-config", candidate_state_path ]);
|
|
if (patch_result.status != 0) {
|
|
release_reload_lock();
|
|
return patch_result.status;
|
|
}
|
|
|
|
let fields = split(trim(patch_result.output), "\t");
|
|
let changed = as_string(fields[0]) == "1";
|
|
let backup_path = length(fields) > 1 ? as_string(fields[1]) : "";
|
|
let status = 0;
|
|
|
|
if (changed) {
|
|
let sing_box_pid_before = sing_box_runtime_pid();
|
|
status = module_status(STATE_UC, [ "hup-sing-box-runtime" ]);
|
|
if (status == 0)
|
|
status = wait_dns_failover_state(candidate_state_path, 5);
|
|
|
|
if (status != 0) {
|
|
log_message("sing-box SIGHUP DNS reload did not become ready; trying service reload", "warn");
|
|
status = module_status(STATE_UC, [ "reload-sing-box-runtime", sing_box_pid_before, "dns-before", "dns-after" ]);
|
|
if (status == 0)
|
|
status = wait_dns_failover_state(candidate_state_path, 8);
|
|
}
|
|
}
|
|
|
|
if (status == 0 && !module_success(DNS_FAILOVER_UC, [ "commit-state", candidate_state_path ]))
|
|
status = 1;
|
|
|
|
if (status != 0 && backup_path != "") {
|
|
log_message("DNS failover apply failed; restoring the previous sing-box configuration", "error");
|
|
if (module_success(SINGBOX_UC, [ "restore-dns-config", backup_path ])) {
|
|
let sing_box_pid_before_restore = sing_box_runtime_pid();
|
|
module_success(STATE_UC, [ "reload-sing-box-runtime", sing_box_pid_before_restore, "dns-failed", "dns-restored" ]);
|
|
}
|
|
}
|
|
|
|
if (backup_path != "")
|
|
remove_file(backup_path);
|
|
release_reload_lock();
|
|
return status;
|
|
}
|
|
|
|
function reload(reason) {
|
|
let status;
|
|
let force_runtime_reload = as_string(reason || "") == "on_config_change" ? 0 : 1;
|
|
let reload_config_fingerprint = external_config_fingerprint();
|
|
rule_condition_cache_enabled = force_runtime_reload;
|
|
|
|
log_message("Reloading Tachyon", "info");
|
|
|
|
status = validate_start_config();
|
|
if (status != 0)
|
|
return status;
|
|
|
|
status = module_status(SUBSCRIPTION_CACHE_UC, [ "ensure-runtime-dirs" ]);
|
|
if (status != 0)
|
|
return status;
|
|
|
|
if (!module_success(STATE_UC, [ "tachyon-running", RT_TABLE_NAME, NFT_TABLE_NAME, NFT_FAKEIP_MARK ])) {
|
|
log_message("Runtime state is incomplete; restarting Tachyon runtime", "info");
|
|
return finish_reload_status(restart_runtime_for_reload(), reload_config_fingerprint);
|
|
}
|
|
|
|
remove_file(RELOAD_STATE_SNAPSHOT_FILE);
|
|
status = module_status(STATE_UC, [
|
|
"capture-reload-state",
|
|
RELOAD_STATE_SNAPSHOT_FILE,
|
|
as_string(RELOAD_STATE_FORMAT)
|
|
]);
|
|
if (status != 0)
|
|
return status;
|
|
|
|
let current_reload_state_file = trim(command_output_from_args([ "mktemp" ]));
|
|
if (current_reload_state_file == "")
|
|
return abort_reload(1, false);
|
|
|
|
status = module_status(STATE_UC, [
|
|
"write-captured-reload-state",
|
|
current_reload_state_file,
|
|
RELOAD_STATE_SNAPSHOT_FILE,
|
|
as_string(RELOAD_STATE_FORMAT),
|
|
"",
|
|
"0",
|
|
"0"
|
|
]);
|
|
if (status != 0) {
|
|
remove_file(current_reload_state_file);
|
|
return abort_reload(status, false);
|
|
}
|
|
|
|
let dnsmasq_managed_state = dnsmasq_has_tachyon_managed_state() ? 1 : 0;
|
|
let list_update_sources = module_success(STATE_UC, [ "has-list-update-sources" ]) ? 1 : 0;
|
|
let nft_list_update_sources = module_success(STATE_UC, [ "has-nft-list-update-sources" ]) ? 1 : 0;
|
|
let runtime_cache_needs_rebuild = 0;
|
|
if (as_string(getenv("TACHYON_RUNTIME_CACHE_INVALIDATED") || "0") == "1" ||
|
|
module_success(SUBSCRIPTION_CACHE_UC, [ "runtime-cache-needs-rebuild", SECTION_CACHE_DIR ]))
|
|
runtime_cache_needs_rebuild = 1;
|
|
|
|
let plan_result = module_capture(RELOAD_UC, [
|
|
"plan-state-files",
|
|
RELOAD_STATE_FILE,
|
|
current_reload_state_file,
|
|
as_string(force_runtime_reload),
|
|
as_string(dnsmasq_managed_state),
|
|
as_string(list_update_sources),
|
|
as_string(nft_list_update_sources),
|
|
as_string(runtime_cache_needs_rebuild)
|
|
]);
|
|
remove_file(current_reload_state_file);
|
|
|
|
if (plan_result.status != 0) {
|
|
if (plan_result.status == 2) {
|
|
log_message("Reload state is unavailable; restarting Tachyon runtime", "info");
|
|
return finish_reload_status(restart_runtime_for_reload(), reload_config_fingerprint);
|
|
}
|
|
return abort_reload(plan_result.status, false);
|
|
}
|
|
|
|
let plan = parse_reload_plan(plan_result.output);
|
|
write_service_trigger_sync_state(plan.changed_service_triggers);
|
|
|
|
if (plan.has_work == 0) {
|
|
status = module_status(STATE_UC, [
|
|
"write-captured-reload-state",
|
|
RELOAD_STATE_FILE,
|
|
RELOAD_STATE_SNAPSHOT_FILE,
|
|
as_string(RELOAD_STATE_FORMAT),
|
|
RULE_CONDITION_CACHE_DIR,
|
|
"1",
|
|
"1"
|
|
]);
|
|
if (status == 0)
|
|
log_message("Reload skipped: runtime-relevant configuration is unchanged", "info");
|
|
return finish_reload_status(status, reload_config_fingerprint);
|
|
}
|
|
|
|
let actions = reload_actions_summary(plan);
|
|
if (actions != "")
|
|
log_message("Applying reload changes: " + actions, "info");
|
|
|
|
if (plan.needs_zapret_restart == 1)
|
|
module_success(ZAPRET_UC, [ "stop-runtime" ]);
|
|
if (plan.needs_zapret2_restart == 1)
|
|
module_success(ZAPRET2_UC, [ "stop-runtime" ]);
|
|
if (plan.needs_byedpi_restart == 1)
|
|
module_success(BYEDPI_UC, [ "stop-runtime" ]);
|
|
|
|
if (plan.needs_nft_rebuild == 1) {
|
|
log_message("Rebuilding nftables rules", "info");
|
|
status = nft_rebuild_runtime();
|
|
if (status != 0)
|
|
return abort_reload(status, true);
|
|
}
|
|
|
|
if (plan.needs_sing_box_reload == 1) {
|
|
module_success(DNS_FAILOVER_UC, [ "stop-runtime" ]);
|
|
module_success(PRIORITY_UC, [ "stop-runtime" ]);
|
|
status = module_status(SINGBOX_UC, [ "configure-service" ]);
|
|
if (status != 0)
|
|
return abort_reload(status, true);
|
|
let sing_box_config_path = config_get(CONFIG_NAME + ".settings.config_path", "");
|
|
let sing_box_config_hash_before = file_md5(sing_box_config_path);
|
|
let sing_box_pid_result = module_capture(STATE_UC, [ "sing-box-service-runtime-pid" ]);
|
|
let sing_box_pid_before = sing_box_pid_result.status == 0 ? trim(sing_box_pid_result.output) : "";
|
|
nft_populate_enabled = plan.needs_nft_rebuild == 1 ? 1 : 0;
|
|
status = singbox_init_config();
|
|
if (status != 0)
|
|
return abort_reload(status, true);
|
|
nft_populate_enabled = NFT_POPULATE_ENABLED_DEFAULT;
|
|
status = module_status(STATE_UC, [
|
|
"reload-sing-box-runtime",
|
|
sing_box_pid_before,
|
|
sing_box_config_hash_before,
|
|
file_md5(sing_box_config_path),
|
|
as_string(force_runtime_reload)
|
|
]);
|
|
if (status != 0)
|
|
return abort_reload(status, true);
|
|
status = module_status(STATE_UC, [
|
|
"wait-sing-box-service-stable",
|
|
as_string(SING_BOX_START_STABLE_MIN_AGE),
|
|
as_string(SING_BOX_START_VERIFY_TIMEOUT)
|
|
]);
|
|
if (status != 0) {
|
|
log_message("Reload verification failed after sing-box was reloaded; stopping Tachyon runtime", "fatal");
|
|
cleanup_failed_runtime();
|
|
return status;
|
|
}
|
|
status = module_status(PRIORITY_UC, [ "start-runtime" ]);
|
|
if (status != 0) {
|
|
log_message("Failed to start Priority runtime after sing-box reload", "fatal");
|
|
cleanup_failed_runtime();
|
|
return status;
|
|
}
|
|
status = module_status(DNS_FAILOVER_UC, [ "start-runtime" ]);
|
|
if (status != 0) {
|
|
log_message("Failed to restart DNS failover runtime after sing-box reload", "fatal");
|
|
cleanup_failed_runtime();
|
|
return status;
|
|
}
|
|
}
|
|
else if (plan.needs_nft_rebuild == 1 && nft_populate_enabled == 1) {
|
|
status = nft_populate_runtime_sets();
|
|
if (status != 0)
|
|
return abort_reload(status, true);
|
|
}
|
|
|
|
if (plan.needs_zapret_restart == 1)
|
|
module_success(ZAPRET_UC, [ "start-runtime" ]);
|
|
if (plan.needs_zapret2_restart == 1)
|
|
module_success(ZAPRET2_UC, [ "start-runtime" ]);
|
|
if (plan.needs_byedpi_restart == 1)
|
|
module_success(BYEDPI_UC, [ "start-runtime" ]);
|
|
|
|
if (plan.needs_dnsmasq_configure == 1) {
|
|
status = dnsmasq_configure(true);
|
|
if (status != 0)
|
|
return abort_reload(status, true);
|
|
module_success(STATE_UC, [ "capture-reload-state", RELOAD_STATE_SNAPSHOT_FILE, as_string(RELOAD_STATE_FORMAT) ]);
|
|
}
|
|
else if (plan.needs_dnsmasq_restore == 1) {
|
|
status = dnsmasq_restore(true);
|
|
if (status != 0)
|
|
return abort_reload(status, true);
|
|
module_success(STATE_UC, [ "capture-reload-state", RELOAD_STATE_SNAPSHOT_FILE, as_string(RELOAD_STATE_FORMAT) ]);
|
|
}
|
|
|
|
if (plan.needs_cron_refresh == 1) {
|
|
status = refresh_cron();
|
|
if (status != 0)
|
|
return abort_reload(status, false);
|
|
}
|
|
|
|
if (plan.needs_list_update == 1)
|
|
module_background(UPDATES_UC, [ "list-update" ]);
|
|
|
|
return finish_reload_status(module_status(STATE_UC, [
|
|
"write-captured-reload-state",
|
|
RELOAD_STATE_FILE,
|
|
RELOAD_STATE_SNAPSHOT_FILE,
|
|
as_string(RELOAD_STATE_FORMAT),
|
|
RULE_CONDITION_CACHE_DIR,
|
|
"1",
|
|
"1"
|
|
]), reload_config_fingerprint);
|
|
}
|
|
|
|
function reload_tracked(reason) {
|
|
if (as_string(getenv("TACHYON_UI_ACTION_TRACKED") || "0") == "1")
|
|
return reload(reason);
|
|
|
|
let job_id = trim(module_output(UI_UC, [ "service-action-begin-if-idle", "reload", "runtime_reload" ]));
|
|
if (job_id != "")
|
|
module_success(UI_UC, [ "service-action-update-pid", job_id, owner_pid() ]);
|
|
|
|
let status = reload(reason);
|
|
if (job_id != "")
|
|
module_success(UI_UC, [ "service-action-finish-after-command", "reload", job_id, as_string(status) ]);
|
|
|
|
return status;
|
|
}
|
|
|
|
function restart() {
|
|
log_message("Restarting Tachyon", "info");
|
|
|
|
let selector_state = capture_selector_state();
|
|
let status = stop_impl();
|
|
if (status != 0)
|
|
return status;
|
|
|
|
status = start_impl();
|
|
if (status != 0) {
|
|
cleanup_failed_runtime();
|
|
return status;
|
|
}
|
|
|
|
if (module_success(STATE_UC, [
|
|
"tachyon-stably-running",
|
|
RT_TABLE_NAME,
|
|
NFT_TABLE_NAME,
|
|
NFT_FAKEIP_MARK,
|
|
as_string(RUNTIME_STABLE_MIN_AGE)
|
|
])) {
|
|
restore_selector_state(selector_state);
|
|
return 0;
|
|
}
|
|
|
|
log_message("Restart verification failed after Tachyon was started; stopping Tachyon runtime", "fatal");
|
|
cleanup_failed_runtime();
|
|
return 1;
|
|
}
|
|
|
|
function package_manager_remove_if_installed(package_name) {
|
|
package_name = as_string(package_name);
|
|
if (command_success_from_args([ "sh", "-c", "command -v apk" ])) {
|
|
if (command_success_from_args([ "apk", "info", "-e", package_name ]))
|
|
command_success_from_args([ "apk", "del", package_name ]);
|
|
return;
|
|
}
|
|
|
|
if (module_success(PACKAGES_UC, [ "opkg-installed", package_name ]))
|
|
command_success_from_args([ "opkg", "remove", "--force-depends", package_name ]);
|
|
}
|
|
|
|
function uninstall() {
|
|
log_message("Uninstalling Tachyon", "info");
|
|
|
|
if (fs.stat(SERVICE_INIT) != null) {
|
|
stop();
|
|
command_success_from_args([ SERVICE_INIT, "disable" ]);
|
|
}
|
|
|
|
dnsmasq_restore_fail_safe();
|
|
|
|
if (fs.stat("/etc/init.d/tachyon") != null) {
|
|
command_success_from_args([ "/etc/init.d/tachyon", "stop" ]);
|
|
command_success_from_args([ "/etc/init.d/tachyon", "disable" ]);
|
|
}
|
|
|
|
package_manager_remove_if_installed("luci-i18n-tachyon-ru");
|
|
package_manager_remove_if_installed("luci-app-tachyon");
|
|
|
|
if (module_success(SINGBOX_UC, [ "managed-service-installed" ])) {
|
|
module_success(SINGBOX_UC, [ "remove-managed-service-script" ]);
|
|
remove_file("/usr/bin/sing-box");
|
|
remove_file("/usr/lib/libcronet.so");
|
|
}
|
|
|
|
command_success_from_args([ "rm", "-rf", "/usr/lib/tachyon" ]);
|
|
command_success_from_args([ "rm", "-rf", LUCI_VIEW_DIR ]);
|
|
remove_file(SERVICE_INIT);
|
|
remove_file(BIN_PATH);
|
|
remove_file("/usr/share/luci/menu.d/luci-app-tachyon.json");
|
|
remove_file("/usr/share/rpcd/acl.d/luci-app-tachyon.json");
|
|
remove_file("/etc/uci-defaults/50_luci-tachyon");
|
|
command_success_from_args([ "find", "/usr/lib/lua/luci/i18n", "-maxdepth", "1", "-type", "f", "-name", LUCI_I18N_DOMAIN + ".*.lmo", "-delete" ]);
|
|
remove_file("/usr/lib/lua/luci/i18n/" + LUCI_I18N_DOMAIN + ".ru.lua");
|
|
remove_file("/usr/lib/lua/luci/i18n/" + LUCI_I18N_DOMAIN + ".en.lua");
|
|
command_success("rm -f /var/luci-indexcache* /tmp/luci-indexcache* 2>/dev/null");
|
|
if (fs.stat("/etc/init.d/rpcd") != null)
|
|
command_success_from_args([ "/etc/init.d/rpcd", "reload" ]);
|
|
|
|
print("{\"removed\":true}\n");
|
|
return 0;
|
|
}
|
|
|
|
function enable_service() {
|
|
return command_status_from_args([ SERVICE_INIT, "enable" ]);
|
|
}
|
|
|
|
function disable_service() {
|
|
return command_status_from_args([ SERVICE_INIT, "disable" ]);
|
|
}
|
|
|
|
let mode = ARGV[0] || "";
|
|
let status = 1;
|
|
|
|
if (mode == "main")
|
|
status = start_main();
|
|
else if (mode == "start")
|
|
status = start();
|
|
else if (mode == "stop")
|
|
status = stop();
|
|
else if (mode == "reload-firewall")
|
|
status = reload_firewall();
|
|
else if (mode == "reload")
|
|
status = reload_tracked(ARGV[1] || "");
|
|
else if (mode == "dns-failover-apply")
|
|
status = dns_failover_apply(ARGV[1] || "");
|
|
else if (mode == "restart")
|
|
status = restart();
|
|
else if (mode == "enable")
|
|
status = enable_service();
|
|
else if (mode == "disable")
|
|
status = disable_service();
|
|
else if (mode == "selector-state-from-proxies-fixture") {
|
|
write_json(selector_state_from_proxies_payload(read_json_file(ARGV[1])));
|
|
status = 0;
|
|
}
|
|
else if (mode == "selector-restore-pairs-fixture") {
|
|
write_json(selector_restore_pairs(read_json_file(ARGV[1]), read_json_file(ARGV[2])));
|
|
status = 0;
|
|
}
|
|
else if (mode == "dnsmasq-restore" || mode == "restore-dnsmasq")
|
|
status = dnsmasq_restore_fail_safe();
|
|
else if (mode == "uninstall")
|
|
status = uninstall();
|
|
else {
|
|
warn("Usage: service/lifecycle.uc <start|stop|reload|restart|main|enable|disable|dnsmasq-restore|uninstall> ...\n");
|
|
status = 1;
|
|
}
|
|
|
|
release_start_subscription_update_lock();
|
|
exit(status); |