926 lines
32 KiB
Ucode
926 lines
32 KiB
Ucode
#!/usr/bin/env ucode
|
|
|
|
let fs = require("fs");
|
|
let uci_core = require("core.uci");
|
|
let common = require("core.common");
|
|
let runtime_dns = require("singbox.dns");
|
|
|
|
const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon";
|
|
const LIB_DIR = getenv("TACHYON_LIB") || "/usr/lib/tachyon";
|
|
const TMP_SING_BOX_FOLDER = getenv("TMP_SING_BOX_FOLDER") || "/tmp/sing-box";
|
|
const TMP_RULESET_FOLDER = getenv("TMP_RULESET_FOLDER") || TMP_SING_BOX_FOLDER + "/rulesets";
|
|
const TMP_SUBSCRIPTION_FOLDER = getenv("TMP_SUBSCRIPTION_FOLDER") || TMP_SING_BOX_FOLDER + "/subscriptions";
|
|
const RUNTIME_STATE_DIR = getenv("TACHYON_RUNTIME_STATE_DIR") || "/var/run/tachyon";
|
|
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 RUNTIME_CACHE_FORMAT_FILE = getenv("TACHYON_RUNTIME_CACHE_FORMAT_FILE") || RUNTIME_STATE_DIR + "/cache-format";
|
|
const RUNTIME_CACHE_FORMAT = getenv("TACHYON_RUNTIME_CACHE_FORMAT") || "8";
|
|
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 PENDING_RELOAD_FILE = getenv("TACHYON_PENDING_RELOAD_FILE") || RUNTIME_STATE_DIR + "/reload.pending";
|
|
const SERVICE_INIT = getenv("TACHYON_SERVICE_INIT") || "/etc/init.d/tachyon";
|
|
const NFT_TABLE_NAME = getenv("NFT_TABLE_NAME") || "TachyonTable";
|
|
const NFT_COMMON_SET_NAME = getenv("NFT_COMMON_SET_NAME") || "tachyon_subnets";
|
|
const NFT_COMMON6_SET_NAME = getenv("NFT_COMMON6_SET_NAME") || "tachyon_subnets6";
|
|
const NFT_PORT_SET_NAME = getenv("NFT_PORT_SET_NAME") || "tachyon_ports";
|
|
const NFT_IP_PORT_SET_NAME = getenv("NFT_IP_PORT_SET_NAME") || "tachyon_ip_ports";
|
|
const NFT_IP_PORT6_SET_NAME = getenv("NFT_IP_PORT6_SET_NAME") || "tachyon_ip6_ports";
|
|
const NFT_INTERFACE_SET_NAME = getenv("NFT_INTERFACE_SET_NAME") || "tachyon_interfaces";
|
|
const NFT_LOCALV4_SET_NAME = getenv("NFT_LOCALV4_SET_NAME") || "localv4";
|
|
const NFT_LOCALV6_SET_NAME = getenv("NFT_LOCALV6_SET_NAME") || "localv6";
|
|
const NFT_FAKEIP_MARK = getenv("NFT_FAKEIP_MARK") || "0x00100000";
|
|
const SB_SERVICE_MIXED_INBOUND_ADDRESS = getenv("SB_SERVICE_MIXED_INBOUND_ADDRESS") || "127.0.0.1";
|
|
const SB_SERVICE_MIXED_INBOUND_PORT = getenv("SB_SERVICE_MIXED_INBOUND_PORT") || "4534";
|
|
const SB_VARIANT_STATE_FILE = getenv("SB_VARIANT_STATE_FILE") || "/etc/tachyon/sing-box-variant";
|
|
const SB_VERSION_STATE_FILE = getenv("SB_VERSION_STATE_FILE") || "/etc/tachyon/sing-box-version";
|
|
const SB_MANAGED_SERVICE_MARKER = getenv("SB_MANAGED_SERVICE_MARKER") || "Tachyon managed sing-box service for binary variants";
|
|
|
|
function as_string(value) {
|
|
return value == null ? "" : "" + value;
|
|
}
|
|
|
|
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 command_env(assignments) {
|
|
let parts = [];
|
|
for (let name, value in assignments)
|
|
push(parts, name + "=" + shell_quote(value));
|
|
return join(" ", parts);
|
|
}
|
|
|
|
function command_output(command) {
|
|
let pipe = fs.popen(command, "r");
|
|
if (!pipe)
|
|
return "";
|
|
|
|
let data = pipe.read("all");
|
|
let status = pipe.close();
|
|
if (status != 0 || data == null)
|
|
return "";
|
|
return as_string(data);
|
|
}
|
|
|
|
function command_output_from_args(args) {
|
|
return command_output(command_from_args(args));
|
|
}
|
|
|
|
function command_status(command) {
|
|
let status = int(system(command));
|
|
return status > 255 ? int(status / 256) : status;
|
|
}
|
|
|
|
function command_success_from_args(args) {
|
|
return system(command_from_args(args) + " >/dev/null 2>&1") == 0;
|
|
}
|
|
|
|
function command_exists(name) {
|
|
return command_success_from_args([ "command", "-v", name ]);
|
|
}
|
|
|
|
function write_file(path, value) {
|
|
return fs.writefile(as_string(path), as_string(value)) != null;
|
|
}
|
|
|
|
function remove_file(path) {
|
|
try {
|
|
fs.unlink(as_string(path));
|
|
}
|
|
catch (e) {
|
|
}
|
|
}
|
|
|
|
function remove_files(paths) {
|
|
for (let path in paths)
|
|
if (as_string(path) != "")
|
|
remove_file(path);
|
|
}
|
|
|
|
function file_first_line(path) {
|
|
let data = fs.readfile(as_string(path));
|
|
if (data == null)
|
|
return "";
|
|
let newline = index(data, "\n");
|
|
return trim(newline >= 0 ? substr(data, 0, newline) : data);
|
|
}
|
|
|
|
function object_or_empty(value) {
|
|
return type(value) == "object" ? value : {};
|
|
}
|
|
|
|
function array_or_empty(value) {
|
|
return type(value) == "array" ? value : [];
|
|
}
|
|
|
|
function option(section, key, fallback) {
|
|
if (fallback == null)
|
|
fallback = "";
|
|
|
|
let value = object_or_empty(section)[key];
|
|
if (value == null)
|
|
return fallback;
|
|
if (type(value) == "array")
|
|
return join(" ", value);
|
|
return as_string(value);
|
|
}
|
|
|
|
function arg_bool(value) {
|
|
value = lc(as_string(value));
|
|
return value == "true" || value == "1" || value == "yes" || value == "on";
|
|
}
|
|
|
|
function bool_option(section, key, fallback) {
|
|
let value = object_or_empty(section)[key];
|
|
return value == null ? !!fallback : arg_bool(value);
|
|
}
|
|
|
|
function whitespace_items(value) {
|
|
let result = [];
|
|
if (type(value) == "array") {
|
|
for (let item in value) {
|
|
item = as_string(item);
|
|
if (item != "")
|
|
push(result, item);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
for (let item in split(trim(as_string(value)), /[ \t\r\n]+/))
|
|
if (item != "")
|
|
push(result, item);
|
|
return result;
|
|
}
|
|
|
|
function file_exists(path) {
|
|
return fs.stat(as_string(path)) != null;
|
|
}
|
|
|
|
function parent_dir(path) {
|
|
path = as_string(path);
|
|
let slash = rindex(path, "/");
|
|
return slash >= 0 ? substr(path, 0, slash) : "";
|
|
}
|
|
|
|
function ensure_dir(path) {
|
|
return command_success_from_args([ "mkdir", "-p", path ]);
|
|
}
|
|
|
|
function ensure_parent_dir(path) {
|
|
let dir = parent_dir(path);
|
|
return dir == "" || dir == "." || ensure_dir(dir);
|
|
}
|
|
|
|
function temp_path() {
|
|
return trim(command_output_from_args([ "mktemp" ]));
|
|
}
|
|
|
|
function md5_file(path) {
|
|
let line = trim(command_output_from_args([ "md5sum", as_string(path) ]));
|
|
return length(line) >= 32 ? substr(line, 0, 32) : "";
|
|
}
|
|
|
|
function first_line_last_field(value) {
|
|
value = as_string(value);
|
|
let newline = index(value, "\n");
|
|
let line = trim(newline >= 0 ? substr(value, 0, newline) : value);
|
|
let fields = split(line, /[ \t\r\n]+/);
|
|
return length(fields) > 0 ? as_string(fields[length(fields) - 1]) : "";
|
|
}
|
|
|
|
function sing_box_version_output() {
|
|
return command_exists("sing-box") ? command_output_from_args([ "sing-box", "version" ]) : "";
|
|
}
|
|
|
|
function sing_box_marker_is(value) {
|
|
return file_first_line(SB_VARIANT_STATE_FILE) == as_string(value);
|
|
}
|
|
|
|
function sing_box_version_state() {
|
|
return file_first_line(SB_VERSION_STATE_FILE);
|
|
}
|
|
|
|
function sing_box_write_version_state(version) {
|
|
version = as_string(version);
|
|
return version != "" && ensure_parent_dir(SB_VERSION_STATE_FILE) && write_file(SB_VERSION_STATE_FILE, version + "\n");
|
|
}
|
|
|
|
function sing_box_clear_version_state() {
|
|
remove_file(SB_VERSION_STATE_FILE);
|
|
return true;
|
|
}
|
|
|
|
function sing_box_restore_version_state(version) {
|
|
version = as_string(version);
|
|
return version != "" ? sing_box_write_version_state(version) : sing_box_clear_version_state();
|
|
}
|
|
|
|
function sing_box_variant_marker() {
|
|
return file_first_line(SB_VARIANT_STATE_FILE);
|
|
}
|
|
|
|
function sing_box_write_variant_marker(variant) {
|
|
variant = as_string(variant);
|
|
return variant != "" && ensure_parent_dir(SB_VARIANT_STATE_FILE) && write_file(SB_VARIANT_STATE_FILE, variant + "\n");
|
|
}
|
|
|
|
function sing_box_clear_variant_marker() {
|
|
remove_file(SB_VARIANT_STATE_FILE);
|
|
return true;
|
|
}
|
|
|
|
function sing_box_restore_variant_marker(variant) {
|
|
variant = as_string(variant);
|
|
return variant != "" ? sing_box_write_variant_marker(variant) : sing_box_clear_variant_marker();
|
|
}
|
|
|
|
function sing_box_version() {
|
|
if (!command_exists("sing-box"))
|
|
return "";
|
|
if (sing_box_marker_is("extended-compressed"))
|
|
return sing_box_version_state();
|
|
return first_line_last_field(sing_box_version_output());
|
|
}
|
|
|
|
function sing_box_version_is_extended(value) {
|
|
return index(as_string(value), "extended") >= 0;
|
|
}
|
|
|
|
function sing_box_is_extended(value) {
|
|
value = as_string(value);
|
|
if (value == "" && command_exists("sing-box") && (sing_box_marker_is("extended-compressed") || sing_box_marker_is("extended")))
|
|
return true;
|
|
|
|
return sing_box_version_is_extended(value != "" ? value : sing_box_version());
|
|
}
|
|
|
|
function output_has_build_tag(output, tag) {
|
|
tag = as_string(tag);
|
|
if (tag == "")
|
|
return false;
|
|
|
|
for (let item in split(trim(replace(as_string(output), /[,: \t\r\n]+/g, " ")), " "))
|
|
if (as_string(item) == tag)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
function sing_box_supports_tailscale(version, version_output) {
|
|
version = as_string(version);
|
|
version_output = as_string(version_output);
|
|
|
|
if (command_exists("sing-box") && sing_box_marker_is("extended-compressed"))
|
|
return true;
|
|
if (sing_box_is_extended(version))
|
|
return true;
|
|
if (version_output != "")
|
|
return output_has_build_tag(version_output, "with_tailscale");
|
|
return output_has_build_tag(sing_box_version_output(), "with_tailscale");
|
|
}
|
|
|
|
function module_command(args) {
|
|
let command_args = [ "ucode", "-L", LIB_DIR ];
|
|
for (let arg in args)
|
|
push(command_args, arg);
|
|
return command_from_args(command_args);
|
|
}
|
|
|
|
function module_success(args) {
|
|
return command_status(module_command(args)) == 0;
|
|
}
|
|
|
|
function sing_box_package_installed(name) {
|
|
return module_success([ LIB_DIR + "/core/packages.uc", "installed", as_string(name) ]);
|
|
}
|
|
|
|
function sing_box_is_tiny(version, version_output) {
|
|
version = as_string(version);
|
|
version_output = as_string(version_output);
|
|
|
|
if (command_exists("sing-box") && sing_box_marker_is("extended-compressed"))
|
|
return false;
|
|
if (sing_box_is_extended(version != "" ? version : sing_box_version()))
|
|
return false;
|
|
if (sing_box_package_installed("sing-box-tiny"))
|
|
return true;
|
|
if (!sing_box_marker_is("tiny"))
|
|
return false;
|
|
return !sing_box_supports_tailscale(version, version_output);
|
|
}
|
|
|
|
function sing_box_variant() {
|
|
let version = "";
|
|
|
|
if (!command_exists("sing-box"))
|
|
return "not-installed";
|
|
if (sing_box_marker_is("extended-compressed"))
|
|
return "extended-compressed";
|
|
|
|
version = sing_box_version();
|
|
if (sing_box_is_extended(version))
|
|
return sing_box_marker_is("extended-compressed") ? "extended-compressed" : "extended";
|
|
if (sing_box_is_tiny(version, ""))
|
|
return "tiny";
|
|
return "stable";
|
|
}
|
|
|
|
function log_message(message, level) {
|
|
level = as_string(level || "info");
|
|
command_success_from_args([ "logger", "-t", "tachyon", "[" + level + "] " + as_string(message) ]);
|
|
}
|
|
|
|
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 uci_settings() {
|
|
return object_or_empty(uci_core.get_all(CONFIG_NAME, "settings"));
|
|
}
|
|
|
|
function uci_path(config, section, key) {
|
|
return as_string(config) + "." + as_string(section) + "." + as_string(key);
|
|
}
|
|
|
|
function uci_get_option(config, section, key) {
|
|
return trim(uci_core.get(uci_path(config, section, key)));
|
|
}
|
|
|
|
function ensure_sing_box_main_section() {
|
|
if (uci_core.exists("sing-box.main"))
|
|
return true;
|
|
return uci_core.set_section("sing-box.main", "sing-box");
|
|
}
|
|
|
|
function uci_set_option(config, section, key, value) {
|
|
if (!uci_core.exists(as_string(config) + "." + as_string(section)))
|
|
return false;
|
|
return uci_core.set(uci_path(config, section, key), value);
|
|
}
|
|
|
|
function uci_commit(config) {
|
|
return uci_core.commit(config);
|
|
}
|
|
|
|
function owner_pid() {
|
|
let pid = trim(command_output_from_args([ "sh", "-c", "echo $PPID" ]));
|
|
return match(pid, /^[0-9]+$/) != null ? pid : "0";
|
|
}
|
|
|
|
function managed_service_installed() {
|
|
let data = fs.readfile("/etc/init.d/sing-box");
|
|
return data != null && index(as_string(data), SB_MANAGED_SERVICE_MARKER) >= 0;
|
|
}
|
|
|
|
function managed_service_text() {
|
|
return "#!/bin/sh /etc/rc.common\n" +
|
|
"# " + SB_MANAGED_SERVICE_MARKER + "\n\n" +
|
|
"USE_PROCD=1\n" +
|
|
"START=99\n" +
|
|
"PROG=\"/usr/bin/sing-box\"\n\n" +
|
|
"start_service() {\n" +
|
|
" config_load \"sing-box\"\n" +
|
|
" local enabled config_file working_directory\n" +
|
|
" local log_stderr\n\n" +
|
|
" config_get_bool enabled \"main\" \"enabled\" \"0\"\n" +
|
|
" [ \"$enabled\" -eq \"1\" ] || return 0\n\n" +
|
|
" config_get config_file \"main\" \"conffile\" \"/etc/sing-box/config.json\"\n" +
|
|
" config_get working_directory \"main\" \"workdir\" \"/usr/share/sing-box\"\n" +
|
|
" config_get_bool log_stderr \"main\" \"log_stderr\" \"1\"\n\n" +
|
|
" procd_open_instance\n" +
|
|
" procd_set_param command \"$PROG\" run -c \"$config_file\" -D \"$working_directory\"\n" +
|
|
" procd_set_param file \"$config_file\"\n" +
|
|
" procd_set_param stderr \"$log_stderr\"\n" +
|
|
" procd_set_param limits core=\"unlimited\"\n" +
|
|
" procd_set_param limits nofile=\"1000000 1000000\"\n" +
|
|
" procd_set_param respawn\n" +
|
|
" procd_close_instance\n" +
|
|
"}\n\n" +
|
|
"service_triggers() {\n" +
|
|
" procd_add_reload_trigger \"sing-box\"\n" +
|
|
"}\n";
|
|
}
|
|
|
|
function sing_box_compressed_marker_set() {
|
|
return trim(as_string(fs.readfile(SB_VARIANT_STATE_FILE) || "")) == "extended-compressed";
|
|
}
|
|
|
|
function install_managed_service_script() {
|
|
let tmp = "/etc/init.d/sing-box.tachyon." + owner_pid();
|
|
if (!write_file(tmp, managed_service_text()))
|
|
return false;
|
|
if (!command_success_from_args([ "chmod", "0755", tmp ])) {
|
|
remove_file(tmp);
|
|
return false;
|
|
}
|
|
return fs.rename(tmp, "/etc/init.d/sing-box");
|
|
}
|
|
|
|
function remove_managed_service_script() {
|
|
if (!managed_service_installed())
|
|
return;
|
|
|
|
command_success_from_args([ "/etc/init.d/sing-box", "stop" ]);
|
|
command_success_from_args([ "/etc/init.d/sing-box", "disable" ]);
|
|
remove_file("/etc/init.d/sing-box");
|
|
}
|
|
|
|
function disable_service_config() {
|
|
if (!ensure_sing_box_main_section())
|
|
return;
|
|
uci_set_option("sing-box", "main", "enabled", "0");
|
|
uci_commit("sing-box");
|
|
}
|
|
|
|
function prepare_service_disabled() {
|
|
disable_service_config();
|
|
if (file_exists("/etc/init.d/sing-box")) {
|
|
command_success_from_args([ "/etc/init.d/sing-box", "stop" ]);
|
|
command_success_from_args([ "/etc/init.d/sing-box", "disable" ]);
|
|
}
|
|
}
|
|
|
|
function configure_service() {
|
|
let settings = uci_settings();
|
|
|
|
if (sing_box_compressed_marker_set() && !install_managed_service_script()) {
|
|
log_message("Failed to install managed sing-box service for compressed binary. Aborted.", "fatal");
|
|
exit(1);
|
|
}
|
|
|
|
if (!ensure_sing_box_main_section())
|
|
exit(1);
|
|
|
|
let changed = false;
|
|
if (uci_get_option("sing-box", "main", "enabled") != "1") {
|
|
if (!uci_set_option("sing-box", "main", "enabled", "1"))
|
|
exit(1);
|
|
changed = true;
|
|
log_message("sing-box service has been enabled", "info");
|
|
}
|
|
|
|
if (uci_get_option("sing-box", "main", "user") != "root") {
|
|
if (!uci_set_option("sing-box", "main", "user", "root"))
|
|
exit(1);
|
|
changed = true;
|
|
log_message("sing-box service user has been changed to root", "info");
|
|
}
|
|
|
|
let config_path = option(settings, "config_path", "");
|
|
let conffile = uci_get_option("sing-box", "main", "conffile");
|
|
if (conffile != config_path) {
|
|
if (!uci_set_option("sing-box", "main", "conffile", config_path))
|
|
exit(1);
|
|
changed = true;
|
|
log_message("sing-box service config path set to " + config_path, "info");
|
|
}
|
|
|
|
if (changed && !uci_commit("sing-box"))
|
|
exit(1);
|
|
|
|
if (file_exists("/etc/rc.d/S99sing-box")) {
|
|
log_message("Disabling standalone sing-box autostart", "info");
|
|
command_success_from_args([ "/etc/init.d/sing-box", "disable" ]);
|
|
}
|
|
}
|
|
|
|
function download_via_proxy_option_for_purpose(purpose) {
|
|
purpose = as_string(purpose || "lists");
|
|
if (purpose == "lists")
|
|
return "download_lists_via_proxy";
|
|
if (purpose == "components")
|
|
return "download_components_via_proxy";
|
|
return "";
|
|
}
|
|
|
|
function download_via_proxy_section_option_for_purpose(purpose) {
|
|
purpose = as_string(purpose || "lists");
|
|
if (purpose == "lists")
|
|
return "download_lists_via_proxy_section";
|
|
if (purpose == "components")
|
|
return "download_components_via_proxy_section";
|
|
return "";
|
|
}
|
|
|
|
function download_via_proxy_section(settings, purpose) {
|
|
let enabled_option = download_via_proxy_option_for_purpose(purpose);
|
|
if (enabled_option == "" || !bool_option(settings, enabled_option, false))
|
|
return "";
|
|
|
|
let section_option = download_via_proxy_section_option_for_purpose(purpose);
|
|
let configured = section_option != "" ? option(settings, section_option, "") : "";
|
|
if (configured != "")
|
|
return configured;
|
|
|
|
return option(settings, "download_lists_via_proxy_section", "");
|
|
}
|
|
|
|
function service_proxy_port_for_purpose(purpose) {
|
|
return int(SB_SERVICE_MIXED_INBOUND_PORT) + (as_string(purpose || "lists") == "components" ? 1 : 0);
|
|
}
|
|
|
|
function service_proxy_address(settings, purpose) {
|
|
return download_via_proxy_section(settings, purpose) != "" ?
|
|
SB_SERVICE_MIXED_INBOUND_ADDRESS + ":" + service_proxy_port_for_purpose(purpose) : "";
|
|
}
|
|
|
|
function ip_addr_first_inet4(data) {
|
|
for (let line in split(as_string(data), "\n")) {
|
|
let matched = match(line, /inet[ \t]+([0-9.]+)\//);
|
|
if (matched)
|
|
return as_string(matched[1]);
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function network_interface_ipv4(name) {
|
|
let data = command_output_from_args([ "ubus", "call", "network.interface." + as_string(name), "status" ]);
|
|
try {
|
|
let value = json(data);
|
|
let addresses = array_or_empty(object_or_empty(value)["ipv4-address"]);
|
|
if (length(addresses) > 0)
|
|
return as_string(object_or_empty(addresses[0]).address);
|
|
}
|
|
catch (e) {
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function device_ipv4_address_value(device) {
|
|
return ip_addr_first_inet4(command_output_from_args([ "ip", "-4", "addr", "show", "dev", as_string(device) ]));
|
|
}
|
|
|
|
function service_listen_address_value(settings) {
|
|
let configured = option(settings, "service_listen_address", "");
|
|
if (configured != "") {
|
|
log_message("service_listen_address is set manually; automatic listen-address detection is skipped", "warn");
|
|
return configured;
|
|
}
|
|
|
|
let address = network_interface_ipv4("lan");
|
|
if (address != "")
|
|
return address;
|
|
|
|
for (let iface in whitespace_items(option(settings, "source_network_interfaces", "br-lan"))) {
|
|
address = network_interface_ipv4(iface);
|
|
if (address != "")
|
|
return address;
|
|
address = device_ipv4_address_value(iface);
|
|
if (address != "")
|
|
return address;
|
|
}
|
|
|
|
log_message("Failed to determine the listening IP address. Please open an issue to report this problem: https://github.com/ushan0v/tachyon/issues", "error");
|
|
return "";
|
|
}
|
|
|
|
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
|
|
};
|
|
}
|
|
|
|
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 log_lines(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 first_nonblank_line(path) {
|
|
let data = as_string(fs.readfile(path) || "");
|
|
for (let line in split(data, "\n"))
|
|
if (trim(as_string(line)) != "")
|
|
return as_string(line);
|
|
return "";
|
|
}
|
|
|
|
function last_nonblank_line(path) {
|
|
let result = "";
|
|
let data = as_string(fs.readfile(path) || "");
|
|
for (let line in split(data, "\n"))
|
|
if (trim(as_string(line)) != "")
|
|
result = as_string(line);
|
|
return result;
|
|
}
|
|
|
|
function generator_failure_reason(path, status) {
|
|
let reason = last_nonblank_line(path);
|
|
return reason != "" ? reason : "exit status " + status;
|
|
}
|
|
|
|
function log_file_lines(path, level, prefix) {
|
|
log_lines(fs.readfile(path), level, prefix);
|
|
}
|
|
|
|
function sing_box_check(config_path, output_path) {
|
|
let status = command_status(
|
|
command_from_args([ "sing-box", "-c", config_path, "check" ]) +
|
|
" >" + shell_quote(output_path) + " 2>&1"
|
|
);
|
|
let reason = status == 0 ? "" : first_nonblank_line(output_path);
|
|
if (status != 0 && reason == "")
|
|
reason = "exit status " + status;
|
|
return { status, reason };
|
|
}
|
|
|
|
function prepare_subscription_caches(prepared, no_refresh) {
|
|
let result = subscription_cache_capture([ "prepare-caches", "runtime", prepared ? "1" : "0", no_refresh ? "1" : "0" ]);
|
|
if (result.status != 0) {
|
|
log_message("Subscription caches are not ready for sing-box config generation. Aborted.", "fatal");
|
|
log_lines(result.output, "debug", "subscription cache: ");
|
|
return null;
|
|
}
|
|
return trim(result.output);
|
|
}
|
|
|
|
function save_config_file(temp_file_path, config_path) {
|
|
let current_hash = md5_file(config_path);
|
|
let temp_hash = md5_file(temp_file_path);
|
|
|
|
if (current_hash != temp_hash) {
|
|
log_message("sing-box configuration changed; updating " + config_path, "info");
|
|
if (!ensure_parent_dir(config_path))
|
|
return false;
|
|
return command_success_from_args([ "mv", "-f", temp_file_path, config_path ]);
|
|
}
|
|
|
|
log_message("sing-box configuration is unchanged", "info");
|
|
remove_file(temp_file_path);
|
|
return true;
|
|
}
|
|
|
|
function replace_dns_server(config, replacement) {
|
|
let servers = array_or_empty(object_or_empty(config.dns).servers);
|
|
for (let i = 0; i < length(servers); i++) {
|
|
if (as_string(object_or_empty(servers[i]).tag) == as_string(replacement.tag)) {
|
|
servers[i] = replacement;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function patch_dns_config(state_path) {
|
|
let settings = uci_settings();
|
|
let candidate_state = common.read_json_file(state_path);
|
|
let expected = runtime_dns.state_template(settings);
|
|
if (!runtime_dns.state_matches(expected, candidate_state)) {
|
|
log_message("DNS failover state does not match the current UCI configuration", "warn");
|
|
exit(2);
|
|
}
|
|
|
|
let config_path = option(settings, "config_path", "");
|
|
let config = common.read_json_file(config_path);
|
|
if (config_path == "" || type(config) != "object") {
|
|
log_message("Cannot read the current sing-box configuration for DNS failover", "error");
|
|
exit(1);
|
|
}
|
|
|
|
let main = runtime_dns.server_config(settings, candidate_state);
|
|
let bootstrap = runtime_dns.bootstrap_config(settings, candidate_state);
|
|
if (main.unsupported || !replace_dns_server(config, main) || !replace_dns_server(config, bootstrap)) {
|
|
log_message("Cannot locate or build canonical DNS servers for failover", "error");
|
|
exit(1);
|
|
}
|
|
|
|
let backup_path = temp_path();
|
|
let temp_config = temp_path();
|
|
if (backup_path == "" || temp_config == "" ||
|
|
fs.writefile(backup_path, fs.readfile(config_path)) == null ||
|
|
!common.write_json_file(temp_config, config)) {
|
|
remove_files([ backup_path, temp_config ]);
|
|
exit(1);
|
|
}
|
|
|
|
let check_log = temp_path();
|
|
let check_result = check_log == ""
|
|
? { status: 1, reason: "unable to create check output file" }
|
|
: sing_box_check(temp_config, check_log);
|
|
if (check_result.status != 0) {
|
|
log_message("DNS failover produced an invalid sing-box configuration: " + check_result.reason, "error");
|
|
remove_files([ backup_path, temp_config, check_log ]);
|
|
exit(1);
|
|
}
|
|
remove_file(check_log);
|
|
|
|
let changed = md5_file(config_path) != md5_file(temp_config);
|
|
if (!save_config_file(temp_config, config_path)) {
|
|
remove_file(backup_path);
|
|
exit(1);
|
|
}
|
|
|
|
if (!changed) {
|
|
remove_file(backup_path);
|
|
print("0\n");
|
|
return;
|
|
}
|
|
|
|
print("1\t", backup_path, "\n");
|
|
}
|
|
|
|
function restore_dns_config(backup_path) {
|
|
let config_path = option(uci_settings(), "config_path", "");
|
|
if (config_path == "" || !file_exists(backup_path))
|
|
return false;
|
|
return command_success_from_args([ "mv", "-f", backup_path, config_path ]);
|
|
}
|
|
|
|
function init_config(populate_nft, caches_prepared, no_refresh) {
|
|
let settings = uci_settings();
|
|
let config_path = option(settings, "config_path", "");
|
|
if (config_path == "") {
|
|
log_message("sing-box config path is empty. Aborted.", "fatal");
|
|
exit(1);
|
|
}
|
|
|
|
let mwan3_active = module_success([ LIB_DIR + "/config/validator.uc", "mwan3-is-active" ]);
|
|
let output_interface = option(settings, "output_network_interface", "");
|
|
if (mwan3_active && output_interface != "")
|
|
log_message("mwan3 is active and Output Network Interface is set to '" + output_interface + "'; sing-box egress is pinned to this interface", "warn");
|
|
else if (mwan3_active)
|
|
log_message("mwan3 is active; disabling sing-box auto_detect_interface so mwan3 can control egress routing", "warn");
|
|
|
|
let deferred_sections = prepare_subscription_caches(caches_prepared, no_refresh);
|
|
if (deferred_sections == null)
|
|
exit(1);
|
|
|
|
let temp_config = temp_path();
|
|
let runtime_log = temp_path();
|
|
if (temp_config == "" || runtime_log == "") {
|
|
remove_files([ temp_config, runtime_log ]);
|
|
exit(1);
|
|
}
|
|
|
|
let generate_status = command_status(
|
|
module_command([
|
|
LIB_DIR + "/singbox/generator.uc",
|
|
"generate-config",
|
|
temp_config,
|
|
service_listen_address_value(settings),
|
|
mwan3_active ? "1" : "0",
|
|
sing_box_is_extended(sing_box_version()) ? "1" : "0"
|
|
]) + " >" + shell_quote(runtime_log) + " 2>&1"
|
|
);
|
|
if (generate_status != 0) {
|
|
let reason = generator_failure_reason(runtime_log, generate_status);
|
|
log_message("Failed to generate sing-box configuration: " + reason, "fatal");
|
|
remove_files([ temp_config, runtime_log ]);
|
|
exit(1);
|
|
}
|
|
log_file_lines(runtime_log, "warn", "sing-box config generator: ");
|
|
|
|
let check_result = sing_box_check(temp_config, runtime_log);
|
|
if (check_result.status != 0) {
|
|
log_message("Generated sing-box configuration is invalid: " + check_result.reason + ". Aborted.", "fatal");
|
|
remove_files([ temp_config, runtime_log ]);
|
|
exit(1);
|
|
}
|
|
|
|
if (populate_nft && !module_success([
|
|
LIB_DIR + "/nft/apply.uc",
|
|
"nft-populate-runtime-sets-from-uci",
|
|
"1",
|
|
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
|
|
])) {
|
|
log_message("Failed to update nftables runtime sets from the generated sing-box configuration. Aborted.", "fatal");
|
|
remove_files([ temp_config, runtime_log ]);
|
|
exit(1);
|
|
}
|
|
|
|
if (!save_config_file(temp_config, config_path)) {
|
|
remove_file(runtime_log);
|
|
exit(1);
|
|
}
|
|
remove_file(runtime_log);
|
|
print(deferred_sections, "\n");
|
|
}
|
|
|
|
let mode = ARGV[0] || "";
|
|
|
|
if (mode == "configure-service")
|
|
configure_service();
|
|
else if (mode == "init-config")
|
|
init_config(arg_bool(ARGV[1] || "1"), arg_bool(ARGV[2] || "0"), arg_bool(ARGV[3] || "0"));
|
|
else if (mode == "save-config-file-fixture")
|
|
exit(save_config_file(ARGV[1] || "", ARGV[2] || "") ? 0 : 1);
|
|
else if (mode == "check-config-fixture") {
|
|
let result = sing_box_check(ARGV[1] || "", ARGV[2] || "");
|
|
if (result.reason != "")
|
|
print(result.reason, "\n");
|
|
exit(result.status);
|
|
}
|
|
else if (mode == "generator-failure-reason-fixture")
|
|
print(generator_failure_reason(ARGV[1] || "", int(ARGV[2] || "1")), "\n");
|
|
else if (mode == "patch-dns-config")
|
|
patch_dns_config(ARGV[1] || "");
|
|
else if (mode == "restore-dns-config")
|
|
exit(restore_dns_config(ARGV[1] || "") ? 0 : 1);
|
|
else if (mode == "managed-service-installed")
|
|
exit(managed_service_installed() ? 0 : 1);
|
|
else if (mode == "remove-managed-service-script")
|
|
remove_managed_service_script();
|
|
else if (mode == "prepare-service-disabled")
|
|
prepare_service_disabled();
|
|
else if (mode == "service-proxy-address")
|
|
print(service_proxy_address(uci_settings(), ARGV[1] || "lists"), "\n");
|
|
else if (mode == "service-listen-address") {
|
|
let address = service_listen_address_value(uci_settings());
|
|
if (address == "")
|
|
exit(1);
|
|
print(address, "\n");
|
|
}
|
|
else if (mode == "device-ipv4-address") {
|
|
let address = device_ipv4_address_value(ARGV[1]);
|
|
if (address == "")
|
|
exit(1);
|
|
print(address, "\n");
|
|
}
|
|
else if (mode == "ip-addr-first-inet4")
|
|
print(ip_addr_first_inet4(fs.readfile("/dev/stdin")), "\n");
|
|
else if (mode == "version")
|
|
print(sing_box_version(), "\n");
|
|
else if (mode == "version-output")
|
|
print(sing_box_version_output());
|
|
else if (mode == "version-from-output")
|
|
print(first_line_last_field(fs.readfile("/dev/stdin")), "\n");
|
|
else if (mode == "read-version-state")
|
|
print(sing_box_version_state(), "\n");
|
|
else if (mode == "write-version-state")
|
|
exit(sing_box_write_version_state(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "clear-version-state")
|
|
exit(sing_box_clear_version_state() ? 0 : 1);
|
|
else if (mode == "restore-version-state")
|
|
exit(sing_box_restore_version_state(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "read-variant-marker")
|
|
print(sing_box_variant_marker(), "\n");
|
|
else if (mode == "write-variant-marker")
|
|
exit(sing_box_write_variant_marker(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "clear-variant-marker")
|
|
exit(sing_box_clear_variant_marker() ? 0 : 1);
|
|
else if (mode == "restore-variant-marker")
|
|
exit(sing_box_restore_variant_marker(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "marker-is")
|
|
exit(sing_box_marker_is(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "is-extended")
|
|
exit(sing_box_is_extended(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "is-tiny")
|
|
exit(sing_box_is_tiny(ARGV[1], ARGV[2]) ? 0 : 1);
|
|
else if (mode == "supports-tailscale")
|
|
exit(sing_box_supports_tailscale(ARGV[1], ARGV[2]) ? 0 : 1);
|
|
else if (mode == "variant")
|
|
print(sing_box_variant(), "\n");
|
|
else {
|
|
warn("Usage: singbox/runtime.uc <operation> ...\n");
|
|
exit(1);
|
|
} |