1174 lines
47 KiB
Ucode
1174 lines
47 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") || "0x04000000";
|
|
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";
|
|
|
|
let as_string = common.as_string;
|
|
let shell_quote = common.shell_quote;
|
|
let command_from_args = common.command_from_args;
|
|
let command_output = common.command_output;
|
|
let command_output_from_args = common.command_output_from_args;
|
|
let command_status = common.command_status;
|
|
let command_success_from_args = common.command_success_from_args;
|
|
let command_status_from_args = common.command_status_from_args;
|
|
let object_or_empty = common.object_or_empty;
|
|
let array_or_empty = common.array_or_empty;
|
|
let option = common.option;
|
|
let bool_option = common.bool_option;
|
|
let file_exists = common.file_exists;
|
|
let remove_file = common.remove_file;
|
|
|
|
function command_env(assignments) {
|
|
let parts = [];
|
|
for (let name, value in assignments)
|
|
push(parts, name + "=" + shell_quote(value));
|
|
return join(" ", parts);
|
|
}
|
|
|
|
function command_output_lenient(command) {
|
|
let pipe = fs.popen(command, "r");
|
|
if (!pipe)
|
|
return "";
|
|
|
|
let data = pipe.read("all");
|
|
pipe.close();
|
|
return data != null ? as_string(data) : "";
|
|
}
|
|
|
|
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_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 arg_bool(value) {
|
|
value = lc(as_string(value));
|
|
return value == "true" || value == "1" || value == "yes" || value == "on";
|
|
}
|
|
|
|
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 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", "/tmp/tachyon-XXXXXX" ]));
|
|
}
|
|
|
|
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]) : "";
|
|
}
|
|
|
|
// The version string is taken from stdout whatever the exit status: sing-box
|
|
// prints "sing-box version X" first and may still exit non-zero (a warning about
|
|
// a missing config, a signal from a concurrent service stop). Gating on the exit
|
|
// code made the dashboard fall back to "unknown" for a perfectly working binary.
|
|
function sing_box_version_output() {
|
|
return command_exists("sing-box") ? command_output_lenient(command_from_args([ "sing-box", "version" ]) + " 2>/dev/null") : "";
|
|
}
|
|
|
|
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") || sing_box_marker_is("lx"))
|
|
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_version_is_lx(value) {
|
|
return index(as_string(value), "-lx") >= 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") || sing_box_marker_is("lx")))
|
|
return true;
|
|
|
|
value = value != "" ? value : sing_box_version();
|
|
return sing_box_version_is_extended(value) || sing_box_version_is_lx(value);
|
|
}
|
|
|
|
function sing_box_is_lx(value) {
|
|
value = as_string(value);
|
|
if (value == "" && command_exists("sing-box") && sing_box_marker_is("lx"))
|
|
return true;
|
|
|
|
return sing_box_version_is_lx(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") || sing_box_marker_is("lx")))
|
|
return true;
|
|
if (sing_box_is_extended(version) || sing_box_is_lx(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 sing_box_supports_xhttp(version, version_output) {
|
|
version = as_string(version);
|
|
version_output = as_string(version_output);
|
|
|
|
if (command_exists("sing-box") && (sing_box_marker_is("extended-compressed") || sing_box_marker_is("lx")))
|
|
return true;
|
|
if (sing_box_is_extended(version) || sing_box_is_lx(version))
|
|
return true;
|
|
if (version_output != "")
|
|
return output_has_build_tag(version_output, "with_xhttp");
|
|
return output_has_build_tag(sing_box_version_output(), "with_xhttp");
|
|
}
|
|
|
|
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") || sing_box_marker_is("lx")))
|
|
return false;
|
|
let computed_version = version != "" ? version : sing_box_version();
|
|
if (sing_box_is_extended(computed_version) || sing_box_is_lx(computed_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 (sing_box_marker_is("lx"))
|
|
return "lx";
|
|
if (sing_box_marker_is("extended-compressed"))
|
|
return "extended-compressed";
|
|
if (!command_exists("sing-box"))
|
|
return "not-installed";
|
|
|
|
version = sing_box_version();
|
|
if (sing_box_is_lx(version))
|
|
return "lx";
|
|
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 && index(as_string(data), "GOGC=\"50\"") >= 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" +
|
|
" [ -d /dev/net ] || mkdir -p /dev/net\n" +
|
|
" [ -c /dev/net/tun ] || mknod /dev/net/tun c 10 200 2>/dev/null || true\n" +
|
|
" modprobe tun 2>/dev/null || true\n" +
|
|
" modprobe inet_diag 2>/dev/null || true\n\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 env GODEBUG=\"madvdontneed=1\" GOGC=\"50\"\n" +
|
|
" procd_set_param term_timeout 15\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 (!managed_service_installed() && !install_managed_service_script()) {
|
|
log_message("Failed to install managed sing-box service. 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" ]);
|
|
}
|
|
|
|
for (let comp_svc in [ "forkop", "podkop", "netshift" ]) {
|
|
if (file_exists("/etc/init.d/" + comp_svc)) {
|
|
command_status_from_args([ "/etc/init.d/" + comp_svc, "stop" ]);
|
|
command_status_from_args([ "/etc/init.d/" + comp_svc, "disable" ]);
|
|
}
|
|
for (let rc_link in (fs.glob("/etc/rc.d/*" + comp_svc) || [])) {
|
|
remove_file(rc_link);
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
// An interface that is down answers with an error rather than a status
|
|
// object. The empty string the caller gets back means the same thing.
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function device_ipv4_address_value(device) {
|
|
return ip_addr_first_inet4(command_output_from_args([ "ip", "-4", "addr", "show", "dev", as_string(device) ]));
|
|
}
|
|
|
|
// Returns true for RFC-1918 private IPv4 addresses (10/8, 172.16/12, 192.168/16).
|
|
// Used to filter out WAN/public addresses when enumerating interfaces.
|
|
function is_private_ipv4(addr) {
|
|
addr = as_string(addr);
|
|
return match(addr, /^10\./) != null ||
|
|
match(addr, /^172\.(1[6-9]|2[0-9]|3[01])\./) != null ||
|
|
match(addr, /^192\.168\./) != null;
|
|
}
|
|
|
|
// Last-resort fallback for non-standard setups (e.g. x86/64 where the LAN
|
|
// interface is named "eth1", "enp2s0", etc. — not "lan" or "br-lan").
|
|
// Calls `ubus call network.interface dump` to get all UCI interfaces, then
|
|
// returns the first private IPv4 found on a non-loopback, non-WAN interface.
|
|
function network_interface_dump_first_private_ipv4() {
|
|
let data = command_output_from_args([ "ubus", "call", "network.interface", "dump" ]);
|
|
try {
|
|
let value = json(data);
|
|
let interfaces = array_or_empty(object_or_empty(value)["interface"]);
|
|
for (let iface in interfaces) {
|
|
iface = object_or_empty(iface);
|
|
let name = as_string(iface["interface"] || "");
|
|
// Skip obviously non-LAN interfaces.
|
|
if (name == "wan" || name == "wan6" || name == "loopback" || name == "")
|
|
continue;
|
|
for (let addr_obj in array_or_empty(iface["ipv4-address"])) {
|
|
let addr = as_string(object_or_empty(addr_obj).address);
|
|
if (addr != "" && is_private_ipv4(addr))
|
|
return addr;
|
|
}
|
|
}
|
|
}
|
|
catch (e) {}
|
|
return "";
|
|
}
|
|
|
|
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", "debug");
|
|
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;
|
|
}
|
|
|
|
// Third-tier fallback: enumerate all UCI interfaces via ubus dump.
|
|
// Handles x86/64 and other non-standard setups where LAN is not named
|
|
// "lan"/"br-lan". Skips wan, wan6, loopback and any public addresses.
|
|
address = network_interface_dump_first_private_ipv4();
|
|
if (address != "") {
|
|
log_message("listen-address auto-detected via interface dump: " + address + ". Set service_listen_address in UCI to pin it.", "warn");
|
|
return address;
|
|
}
|
|
|
|
log_message("Failed to determine the listening IP address. Please open an issue to report this problem: https://github.com/Dushnilin/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 first_error_line(path) {
|
|
let data = as_string(fs.readfile(path) || "");
|
|
for (let line in split(data, "\n")) {
|
|
let text = as_string(line);
|
|
if (trim(text) == "")
|
|
continue;
|
|
if (match(text, /error|fail|invalid|unexpected|near here/i) != null)
|
|
return text;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function generator_failure_reason(path, status) {
|
|
let reason = first_error_line(path);
|
|
if (reason == "")
|
|
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, 2)) {
|
|
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 ]);
|
|
}
|
|
|
|
|
|
const EMPTY_SRS_BINARY = hexdec("5352530278da6200040000ffff00010001");
|
|
|
|
function ensure_local_rulesets_exist(temp_config) {
|
|
try {
|
|
let cfg_raw = as_string(fs.readfile(temp_config) || "");
|
|
let cfg_obj = length(cfg_raw) > 0 ? json(cfg_raw) : null;
|
|
if (type(cfg_obj) == "object" && type(cfg_obj.route) == "object" && type(cfg_obj.route.rule_set) == "array") {
|
|
command_success_from_args(["mkdir", "-p", TMP_RULESET_FOLDER]);
|
|
for (let rs in cfg_obj.route.rule_set) {
|
|
if (type(rs) == "object" && rs.type == "local" && rs.path) {
|
|
let st = fs.stat(rs.path);
|
|
if (!st || st.size < 17) {
|
|
let bname = replace(rs.path, /^.*\//, "");
|
|
let etc_file = "/etc/tachyon/rulesets/" + bname;
|
|
let etc_st = fs.stat(etc_file);
|
|
if (etc_st && etc_st.size >= 17) {
|
|
let content = fs.readfile(etc_file);
|
|
if (content) fs.writefile(rs.path, content);
|
|
} else if (rs.format == "source" || match(rs.path, /\.json$/)) {
|
|
fs.writefile(rs.path, sprintf("%J", { version: 3, rules: [] }));
|
|
} else {
|
|
fs.writefile(rs.path, EMPTY_SRS_BINARY);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// Cleanup stale temp config files from previous failed runs
|
|
try {
|
|
let stale = trim(command_output_from_args([ "find", "/tmp", "-maxdepth", "1", "-name", "config.json.*.tmp.*", "-mmin", "+5", "-type", "f" ]));
|
|
for (let path in split(stale, "\n"))
|
|
if (trim(path) != "") remove_file(trim(path));
|
|
}
|
|
catch (e) {
|
|
// Housekeeping for temp files left by earlier failed runs. Failing here
|
|
// leaves a few stale files in /tmp and must not stop the start.
|
|
}
|
|
|
|
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_supports_xhttp(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");
|
|
log_file_lines(runtime_log, "debug", "sing-box config generator output: ");
|
|
remove_files([ temp_config, runtime_log ]);
|
|
exit(1);
|
|
}
|
|
log_file_lines(runtime_log, "warn", "sing-box config generator: ");
|
|
|
|
ensure_local_rulesets_exist(temp_config);
|
|
let check_result = sing_box_check(temp_config, runtime_log);
|
|
while (check_result.status != 0) {
|
|
// Graceful degradation: if the installed sing-box does not support an inbound
|
|
// field that was added for newer versions (e.g. close_timeout for MTProto
|
|
// hot-reload fix, issue #15, prefer_ip, tolerate_time_skewness), strip it
|
|
// from all inbounds and retry. Loops to handle multiple unknown fields
|
|
// across retries (e.g. prefer_ip then tolerate_time_skewness).
|
|
let user_field_m = match(check_result.reason, /inbounds\[\d+\]\.users\[\d+\]\.(\w+): json: unknown field/);
|
|
let field_m = match(check_result.reason, /inbounds\[\d+\]\.(\w+): json: unknown field/);
|
|
let stripped = false;
|
|
if (user_field_m) {
|
|
let unknown_field = user_field_m[1];
|
|
log_message("Installed sing-box does not support inbound user field '" + unknown_field + "'; retrying without it", "warn");
|
|
let cfg_text = as_string(fs.readfile(temp_config) || "");
|
|
let cfg = length(cfg_text) > 0 ? json(cfg_text) : null;
|
|
if (type(cfg) == "object" && type(cfg.inbounds) == "array") {
|
|
for (let inb in cfg.inbounds) {
|
|
if (type(inb) == "object" && type(inb.users) == "array") {
|
|
for (let u in inb.users) {
|
|
if (type(u) == "object")
|
|
delete u[unknown_field];
|
|
}
|
|
}
|
|
}
|
|
write_file(temp_config, sprintf("%J", cfg));
|
|
stripped = true;
|
|
}
|
|
}
|
|
else if (field_m) {
|
|
let unknown_field = field_m[1];
|
|
log_message("Installed sing-box does not support inbound field '" + unknown_field + "'; retrying without it", "warn");
|
|
let cfg_text = as_string(fs.readfile(temp_config) || "");
|
|
let cfg = length(cfg_text) > 0 ? json(cfg_text) : null;
|
|
if (type(cfg) == "object" && type(cfg.inbounds) == "array") {
|
|
for (let inb in cfg.inbounds) {
|
|
if (type(inb) == "object")
|
|
delete inb[unknown_field];
|
|
}
|
|
write_file(temp_config, sprintf("%J", cfg));
|
|
stripped = true;
|
|
}
|
|
}
|
|
else {
|
|
let ep_field_m = match(check_result.reason, /endpoints\[\d+\]\.(\w+): json: unknown field/);
|
|
let out_field_m = match(check_result.reason, /outbounds\[\d+\]\.(\w+): json: unknown field/);
|
|
if (ep_field_m) {
|
|
let unknown_field = ep_field_m[1];
|
|
log_message("Installed sing-box does not support endpoint field '" + unknown_field + "'; adjusting configuration", "warn");
|
|
let cfg_text = as_string(fs.readfile(temp_config) || "");
|
|
let cfg = length(cfg_text) > 0 ? json(cfg_text) : null;
|
|
if (type(cfg) == "object" && type(cfg.endpoints) == "array") {
|
|
for (let ep in cfg.endpoints) {
|
|
if (type(ep) == "object") {
|
|
if (unknown_field == "amnezia" && type(ep.amnezia) == "object") {
|
|
for (let k, v in ep.amnezia) {
|
|
if (ep[k] == null)
|
|
ep[k] = v;
|
|
}
|
|
}
|
|
delete ep[unknown_field];
|
|
}
|
|
}
|
|
write_file(temp_config, sprintf("%J", cfg));
|
|
stripped = true;
|
|
}
|
|
}
|
|
else if (out_field_m) {
|
|
let unknown_field = out_field_m[1];
|
|
log_message("Installed sing-box does not support outbound field '" + unknown_field + "'; retrying without it", "warn");
|
|
let cfg_text = as_string(fs.readfile(temp_config) || "");
|
|
let cfg = length(cfg_text) > 0 ? json(cfg_text) : null;
|
|
if (type(cfg) == "object" && type(cfg.outbounds) == "array") {
|
|
for (let outb in cfg.outbounds) {
|
|
if (type(outb) == "object")
|
|
delete outb[unknown_field];
|
|
}
|
|
write_file(temp_config, sprintf("%J", cfg));
|
|
stripped = true;
|
|
}
|
|
}
|
|
else if (match(check_result.reason, /experimental\.cache_file\.store_dns/)) {
|
|
log_message("Installed sing-box rejected store_dns: " + check_result.reason + "; switching to store_rdrc and retrying", "warn");
|
|
let cfg_text = as_string(fs.readfile(temp_config) || "");
|
|
let cfg = length(cfg_text) > 0 ? json(cfg_text) : null;
|
|
if (type(cfg) == "object" && type(cfg.experimental) == "object" && type(cfg.experimental.cache_file) == "object") {
|
|
delete cfg.experimental.cache_file.store_dns;
|
|
cfg.experimental.cache_file.store_rdrc = true;
|
|
write_file(temp_config, sprintf("%J", cfg));
|
|
stripped = true;
|
|
}
|
|
}
|
|
else if (match(check_result.reason, /experimental\.cache_file\.store_rdrc/)) {
|
|
log_message("Installed sing-box rejected store_rdrc: " + check_result.reason + "; switching to store_dns and retrying", "warn");
|
|
let cfg_text = as_string(fs.readfile(temp_config) || "");
|
|
let cfg = length(cfg_text) > 0 ? json(cfg_text) : null;
|
|
if (type(cfg) == "object" && type(cfg.experimental) == "object" && type(cfg.experimental.cache_file) == "object") {
|
|
delete cfg.experimental.cache_file.store_rdrc;
|
|
cfg.experimental.cache_file.store_dns = true;
|
|
write_file(temp_config, sprintf("%J", cfg));
|
|
stripped = true;
|
|
}
|
|
}
|
|
else if (match(check_result.reason, /experimental\.cache_file\.(\w+): json: unknown field/)) {
|
|
let cf_m = match(check_result.reason, /experimental\.cache_file\.(\w+): json: unknown field/);
|
|
let unknown_field = cf_m[1];
|
|
log_message("Installed sing-box does not support cache_file field '" + unknown_field + "'; stripping and retrying", "warn");
|
|
let cfg_text = as_string(fs.readfile(temp_config) || "");
|
|
let cfg = length(cfg_text) > 0 ? json(cfg_text) : null;
|
|
if (type(cfg) == "object" && type(cfg.experimental) == "object" && type(cfg.experimental.cache_file) == "object") {
|
|
delete cfg.experimental.cache_file[unknown_field];
|
|
write_file(temp_config, sprintf("%J", cfg));
|
|
stripped = true;
|
|
}
|
|
}
|
|
else if (match(check_result.reason, /outbounds\[\d+\]\.transport: unknown transport type: (\w+)/)) {
|
|
let tr_m = match(check_result.reason, /outbounds\[\d+\]\.transport: unknown transport type: (\w+)/);
|
|
let bad_transport = tr_m[1];
|
|
let out_idx_m = match(check_result.reason, /outbounds\[(\d+)\]/);
|
|
let out_idx = out_idx_m ? int(out_idx_m[1]) : -1;
|
|
log_message("Installed sing-box does not support transport '" + bad_transport + "' on outbound index " + out_idx + "; stripping outbound and retrying", "warn");
|
|
let cfg_text = as_string(fs.readfile(temp_config) || "");
|
|
let cfg = length(cfg_text) > 0 ? json(cfg_text) : null;
|
|
if (type(cfg) == "object" && type(cfg.outbounds) == "array" && out_idx >= 0 && out_idx < length(cfg.outbounds)) {
|
|
let bad_tag = cfg.outbounds[out_idx].tag;
|
|
splice(cfg.outbounds, out_idx, 1);
|
|
for (let outb in cfg.outbounds) {
|
|
if (type(outb) == "object" && type(outb.outbounds) == "array") {
|
|
let filtered = [];
|
|
for (let tag in outb.outbounds) {
|
|
if (tag != bad_tag)
|
|
push(filtered, tag);
|
|
}
|
|
outb.outbounds = filtered;
|
|
}
|
|
}
|
|
write_file(temp_config, sprintf("%J", cfg));
|
|
stripped = true;
|
|
}
|
|
}
|
|
}
|
|
if (!stripped)
|
|
break;
|
|
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-lx")
|
|
exit(sing_box_is_lx(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);
|
|
} |