558 lines
20 KiB
Ucode
558 lines
20 KiB
Ucode
#!/usr/bin/env ucode
|
|
|
|
let fs = require("fs");
|
|
let common = require("core.common");
|
|
let uci_core = require("core.uci");
|
|
let runtime_constants = require("singbox.constants");
|
|
let validator_module = null;
|
|
|
|
let as_string = common.as_string;
|
|
let bool_value = common.bool_value;
|
|
let write_json = common.write_json;
|
|
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 object_or_empty = common.object_or_empty;
|
|
let array_or_empty = common.array_or_empty;
|
|
let option = common.option;
|
|
|
|
const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon";
|
|
const LIB_DIR = getenv("TACHYON_LIB") || "/usr/lib/tachyon";
|
|
const BYEDPI_BIN = getenv("BYEDPI_BIN") || "/usr/bin/ciadpi";
|
|
const BYEDPI_SERVICE_INIT = getenv("BYEDPI_SERVICE_INIT") || "/etc/init.d/byedpi";
|
|
const BYEDPI_STATE_DIR = getenv("BYEDPI_STATE_DIR") || "/var/run/tachyon/byedpi";
|
|
const BYEDPI_PID_DIR = getenv("BYEDPI_PID_DIR") || BYEDPI_STATE_DIR + "/pid";
|
|
const BYEDPI_CHILD_PID_DIR = getenv("BYEDPI_CHILD_PID_DIR") || BYEDPI_STATE_DIR + "/child-pid";
|
|
const BYEDPI_LOG_DIR = getenv("BYEDPI_LOG_DIR") || BYEDPI_STATE_DIR + "/log";
|
|
const BYEDPI_LISTEN_ADDRESS = getenv("BYEDPI_LISTEN_ADDRESS") || "127.0.0.1";
|
|
const BYEDPI_PORT_BASE = getenv("BYEDPI_PORT_BASE") || "1080";
|
|
const BYEDPI_RESPAWN_DELAY = getenv("BYEDPI_RESPAWN_DELAY") || "5";
|
|
const BYEDPI_OPEN_FILES_LIMIT = getenv("BYEDPI_OPEN_FILES_LIMIT") || "4096";
|
|
const BYEDPI_DEFAULT_CMD_OPTS = getenv("BYEDPI_DEFAULT_CMD_OPTS") || "-o 2 --auto=t,r,a,s -d 2";
|
|
const SB_TPROXY_INBOUND_TAG = getenv("SB_TPROXY_INBOUND_TAG") || "tproxy-in";
|
|
|
|
function command_exists(name) {
|
|
return command_success_from_args([ "command", "-v", name ]);
|
|
}
|
|
|
|
let log_message = common.log_message;
|
|
let bool_option = common.bool_option;
|
|
let section_name = common.section_name;
|
|
|
|
function uci_sections(type_name) {
|
|
return uci_core.section_objects(CONFIG_NAME, as_string(type_name));
|
|
}
|
|
|
|
function uci_settings() {
|
|
return object_or_empty(uci_core.get_all(CONFIG_NAME, "settings"));
|
|
}
|
|
|
|
function validator() {
|
|
if (validator_module == null)
|
|
validator_module = require("providers.byedpi.validator");
|
|
return validator_module;
|
|
}
|
|
|
|
function normalize_strategy(value) {
|
|
return validator().strategy_or_default(value, BYEDPI_DEFAULT_CMD_OPTS);
|
|
}
|
|
|
|
function strategy_words(value) {
|
|
value = replace(as_string(value), /[\t\r\n]/g, " ");
|
|
value = replace(value, / +/g, " ");
|
|
value = replace(value, /^ /, "");
|
|
value = replace(value, / $/, "");
|
|
return value == "" ? [] : split(value, " ");
|
|
}
|
|
|
|
function enabled_byedpi_sections() {
|
|
let result = [];
|
|
for (let section in uci_sections("section"))
|
|
if (bool_option(section, "enabled", true) && option(section, "action", "") == "byedpi")
|
|
push(result, section);
|
|
return result;
|
|
}
|
|
|
|
function enabled_rule_count() {
|
|
return length(enabled_byedpi_sections());
|
|
}
|
|
|
|
function rule_port(index_value) {
|
|
return int(BYEDPI_PORT_BASE) + int(index_value) - 1;
|
|
}
|
|
|
|
function ensure_runtime_dirs() {
|
|
return command_success_from_args([ "mkdir", "-p", BYEDPI_STATE_DIR, BYEDPI_PID_DIR, BYEDPI_CHILD_PID_DIR, BYEDPI_LOG_DIR ]);
|
|
}
|
|
|
|
function provider_available() {
|
|
let stat = fs.stat(BYEDPI_BIN);
|
|
return stat != null && stat.mode != null && (int(stat.mode) & 73) != 0;
|
|
}
|
|
|
|
function package_installed() {
|
|
if (command_exists("apk") && command_success_from_args([ "apk", "info", "-e", "byedpi" ]))
|
|
return true;
|
|
if (!command_exists("opkg"))
|
|
return false;
|
|
|
|
for (let line in split(command_output_from_args([ "opkg", "list-installed" ]), "\n")) {
|
|
if (match(trim(as_string(line)), /^byedpi[ \t]+-/))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function first_nonempty_field(value) {
|
|
for (let line in split(as_string(value), "\n")) {
|
|
line = trim(as_string(line));
|
|
if (line == "")
|
|
continue;
|
|
let fields = split(line, /[ \t\r\n]+/);
|
|
return length(fields) > 0 ? as_string(fields[0]) : "";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function package_version() {
|
|
let output = "";
|
|
|
|
if (command_exists("apk") && command_success_from_args([ "apk", "info", "-e", "byedpi" ])) {
|
|
output = command_output_from_args([ "apk", "list", "--installed", "byedpi" ]);
|
|
for (let line in split(output, "\n")) {
|
|
let matched = match(trim(as_string(line)), /^byedpi-([^ \t]+)/);
|
|
if (matched)
|
|
return matched[1];
|
|
}
|
|
|
|
output = command_output_from_args([ "apk", "list", "--installed", "--manifest", "byedpi" ]);
|
|
for (let line in split(output, "\n")) {
|
|
let matched = match(trim(as_string(line)), /^P:byedpi$/);
|
|
if (!matched)
|
|
continue;
|
|
for (let manifest_line in split(output, "\n")) {
|
|
let version_match = match(trim(as_string(manifest_line)), /^V:(.+)$/);
|
|
if (version_match)
|
|
return as_string(version_match[1]);
|
|
}
|
|
}
|
|
|
|
output = command_output_from_args([ "apk", "info", "-v", "byedpi" ]);
|
|
for (let line in split(output, "\n")) {
|
|
line = trim(as_string(line));
|
|
if (line != "")
|
|
return replace(line, /^byedpi-/, "");
|
|
}
|
|
}
|
|
|
|
if (command_exists("opkg")) {
|
|
output = command_output_from_args([ "opkg", "list-installed" ]);
|
|
for (let line in split(output, "\n")) {
|
|
let matched = match(trim(as_string(line)), /^byedpi[ \t]+-[ \t]*(.+)$/);
|
|
if (matched)
|
|
return as_string(matched[1]);
|
|
}
|
|
}
|
|
|
|
if (provider_available())
|
|
return first_nonempty_field(command_output_from_args([ BYEDPI_BIN, "--version" ]));
|
|
return "";
|
|
}
|
|
|
|
function standalone_service_enabled() {
|
|
return fs.stat(BYEDPI_SERVICE_INIT) != null && command_success_from_args([ BYEDPI_SERVICE_INIT, "enabled" ]);
|
|
}
|
|
|
|
function standalone_service_running() {
|
|
return fs.stat(BYEDPI_SERVICE_INIT) != null && command_success_from_args([ BYEDPI_SERVICE_INIT, "status" ]);
|
|
}
|
|
|
|
function runtime_pid_running(pid) {
|
|
pid = as_string(pid);
|
|
return match(pid, /^[0-9]+$/) != null && command_success_from_args([ "kill", "-0", pid ]);
|
|
}
|
|
|
|
let file_first_line = common.file_first_line;
|
|
|
|
function kill_pidfile_process(path, signal) {
|
|
let pid = file_first_line(path);
|
|
if (pid == "")
|
|
return;
|
|
if (signal == "9") {
|
|
if (runtime_pid_running(pid))
|
|
command_success_from_args([ "kill", "-9", pid ]);
|
|
}
|
|
else {
|
|
command_success_from_args([ "kill", pid ]);
|
|
}
|
|
}
|
|
|
|
function pidfiles_in_dir(path) {
|
|
if (fs.stat(path) == null)
|
|
return [];
|
|
|
|
let output = command_output_from_args([ "find", path, "-maxdepth", "1", "-type", "f", "-name", "*.pid" ]);
|
|
let result = [];
|
|
for (let line in split(output, "\n")) {
|
|
line = trim(as_string(line));
|
|
if (line != "")
|
|
push(result, line);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function stop_runtime() {
|
|
for (let pidfile in pidfiles_in_dir(BYEDPI_PID_DIR))
|
|
kill_pidfile_process(pidfile, "");
|
|
for (let pidfile in pidfiles_in_dir(BYEDPI_CHILD_PID_DIR))
|
|
kill_pidfile_process(pidfile, "");
|
|
|
|
command_success_from_args([ "sleep", "1" ]);
|
|
|
|
for (let pidfile in pidfiles_in_dir(BYEDPI_PID_DIR))
|
|
kill_pidfile_process(pidfile, "9");
|
|
for (let pidfile in pidfiles_in_dir(BYEDPI_CHILD_PID_DIR))
|
|
kill_pidfile_process(pidfile, "9");
|
|
|
|
command_success_from_args([ "rm", "-rf", BYEDPI_PID_DIR, BYEDPI_CHILD_PID_DIR, BYEDPI_LOG_DIR ]);
|
|
}
|
|
|
|
function supervisor_command(port, raw_opt, child_pidfile) {
|
|
let args = [ BYEDPI_BIN, "--ip", BYEDPI_LISTEN_ADDRESS, "--port", as_string(port) ];
|
|
for (let word in strategy_words(raw_opt))
|
|
push(args, word);
|
|
|
|
let child_pidfile_q = shell_quote(child_pidfile);
|
|
return "trap 'kill $child 2>/dev/null; rm -f " + child_pidfile_q + "; exit 0' TERM INT; " +
|
|
"ulimit -n " + shell_quote(BYEDPI_OPEN_FILES_LIMIT) + " >/dev/null 2>&1 || true; " +
|
|
command_from_args(args) + " & child=$!; echo $child > " + child_pidfile_q + "; wait $child; rc=$?; rm -f " + child_pidfile_q + "; exit $rc";
|
|
}
|
|
|
|
function supervisor(section, port, raw_opt, child_pidfile) {
|
|
let crash_count = 0;
|
|
let base_delay = int(BYEDPI_RESPAWN_DELAY);
|
|
if (base_delay < 1) base_delay = 5;
|
|
|
|
while (true) {
|
|
if (!provider_available()) {
|
|
print(command_output_from_args([ "date", "+%Y-%m-%d %H:%M:%S" ]), " Provider ", BYEDPI_BIN, " is not executable; retrying in ", base_delay, " seconds\n");
|
|
command_success_from_args([ "sleep", as_string(base_delay) ]);
|
|
continue;
|
|
}
|
|
|
|
let start_time = clock()[0];
|
|
let rc = command_status("sh -c " + shell_quote(supervisor_command(port, raw_opt, child_pidfile)));
|
|
let elapsed = clock()[0] - start_time;
|
|
|
|
if (elapsed < 10) {
|
|
crash_count++;
|
|
if (crash_count > 5) crash_count = 5;
|
|
} else {
|
|
crash_count = 0;
|
|
}
|
|
|
|
let delay = base_delay * (1 + crash_count);
|
|
print(command_output_from_args([ "date", "+%Y-%m-%d %H:%M:%S" ]), " ciadpi for rule ", as_string(section), " exited with code ", rc, "; respawning in ", delay, " seconds\n");
|
|
command_success_from_args([ "sleep", as_string(delay) ]);
|
|
}
|
|
}
|
|
|
|
function start_rule(section, index_value) {
|
|
let name = section_name(section);
|
|
let port = rule_port(index_value);
|
|
let raw_opt = normalize_strategy(option(section, "byedpi_cmd_opts", ""));
|
|
let validation = validator().validate_byedpi_strategy(raw_opt);
|
|
if (!validation.valid) {
|
|
log_message("Invalid ByeDPI strategy for rule '" + name + "': " + validation.message, "fatal");
|
|
return false;
|
|
}
|
|
|
|
let pidfile = BYEDPI_PID_DIR + "/" + name + ".pid";
|
|
let child_pidfile = BYEDPI_CHILD_PID_DIR + "/" + name + ".pid";
|
|
let logfile = BYEDPI_LOG_DIR + "/" + name + ".log";
|
|
|
|
log_message("Starting ciadpi for rule '" + name + "' on " + BYEDPI_LISTEN_ADDRESS + ":" + port, "info");
|
|
let command = common.background_command_with_pid(command_from_args([
|
|
"ucode",
|
|
"-L", LIB_DIR,
|
|
LIB_DIR + "/providers/byedpi/runtime.uc",
|
|
"supervisor",
|
|
name,
|
|
"" + port,
|
|
raw_opt,
|
|
child_pidfile
|
|
]), ">>" + shell_quote(logfile));
|
|
let pid = trim(command_output("sh -c " + shell_quote(command)));
|
|
if (pid == "" || !fs.writefile(pidfile, pid + "\n")) {
|
|
log_message("ciadpi failed to start for rule '" + name + "'. Check " + logfile + ".", "fatal");
|
|
return false;
|
|
}
|
|
|
|
command_success_from_args([ "sleep", "1" ]);
|
|
if (!runtime_pid_running(pid)) {
|
|
log_message("ciadpi failed to start for rule '" + name + "'. Check " + logfile + ".", "fatal");
|
|
return false;
|
|
}
|
|
|
|
let child_pid = file_first_line(child_pidfile);
|
|
if (child_pid == "" || !runtime_pid_running(child_pid))
|
|
log_message("ciadpi supervisor started for rule '" + name + "', but ciadpi is not running yet. Check " + logfile + ".", "warn");
|
|
return true;
|
|
}
|
|
|
|
function start_runtime() {
|
|
stop_runtime();
|
|
|
|
let sections = enabled_byedpi_sections();
|
|
if (length(sections) == 0 || !provider_available())
|
|
return;
|
|
|
|
if (standalone_service_enabled())
|
|
log_message("Standalone byedpi service is enabled. Tachyon manages ciadpi itself for action 'byedpi'; disable standalone byedpi autostart to avoid boot-time port conflicts.", "warn");
|
|
|
|
if (standalone_service_running()) {
|
|
log_message("Stopping standalone byedpi service before starting Tachyon-managed ciadpi runtime", "info");
|
|
command_success_from_args([ BYEDPI_SERVICE_INIT, "stop" ]);
|
|
command_success_from_args([ "sleep", "1" ]);
|
|
if (standalone_service_running()) {
|
|
log_message("Standalone byedpi service is still running and may conflict with Tachyon-managed ciadpi runtime. Aborted.", "fatal");
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
if (!ensure_runtime_dirs()) {
|
|
log_message("Failed to prepare the Tachyon ByeDPI state directory in " + BYEDPI_STATE_DIR + ". Aborted.", "fatal");
|
|
exit(1);
|
|
}
|
|
|
|
let started_sections = [];
|
|
let index_value = 1;
|
|
for (let section in sections) {
|
|
if (!start_rule(section, index_value)) {
|
|
log_message("Rolling back " + as_string(length(started_sections)) + " started ciadpi rules.", "warn");
|
|
for (let started in started_sections) {
|
|
let name = section_name(started);
|
|
let pidfile = BYEDPI_PID_DIR + "/" + name + ".pid";
|
|
let child_pidfile = BYEDPI_CHILD_PID_DIR + "/" + name + ".pid";
|
|
kill_pidfile_process(pidfile, "9");
|
|
kill_pidfile_process(child_pidfile, "9");
|
|
}
|
|
exit(1);
|
|
}
|
|
push(started_sections, section);
|
|
index_value++;
|
|
}
|
|
}
|
|
|
|
function live_pid_count(path) {
|
|
let count = 0;
|
|
for (let pidfile in pidfiles_in_dir(path)) {
|
|
let pid = file_first_line(pidfile);
|
|
if (runtime_pid_running(pid))
|
|
count++;
|
|
else
|
|
fs.unlink(pidfile);
|
|
}
|
|
return count;
|
|
}
|
|
|
|
function restart_count() {
|
|
let count = 0;
|
|
if (fs.stat(BYEDPI_LOG_DIR) == null)
|
|
return count;
|
|
|
|
let output = command_output_from_args([ "find", BYEDPI_LOG_DIR, "-maxdepth", "1", "-type", "f", "-name", "*.log" ]);
|
|
for (let path in split(output, "\n")) {
|
|
path = trim(as_string(path));
|
|
if (path == "")
|
|
continue;
|
|
let data = fs.readfile(path);
|
|
if (data == null)
|
|
continue;
|
|
for (let line in split(data, "\n"))
|
|
if (match(as_string(line), /ciadpi for rule .* exited with code/) != null)
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
function runtime_tag(base, postfix) {
|
|
return runtime_constants.tag(base, postfix);
|
|
}
|
|
|
|
function read_json_file(path) {
|
|
let data = fs.readfile(path);
|
|
if (data == null)
|
|
return null;
|
|
try {
|
|
return json(data);
|
|
}
|
|
catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function value_contains(value, needle) {
|
|
if (type(value) == "array") {
|
|
for (let item in value)
|
|
if (as_string(item) == needle)
|
|
return true;
|
|
return false;
|
|
}
|
|
return as_string(value) == needle;
|
|
}
|
|
|
|
function has_socks_outbound(config, tag, address, port) {
|
|
for (let outbound in array_or_empty(config && config.outbounds)) {
|
|
if (type(outbound) == "object" &&
|
|
outbound.type == "socks" &&
|
|
outbound.tag == tag &&
|
|
outbound.server == address &&
|
|
int(outbound.server_port || 0) == int(port))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function has_route_rule(config, inbound, outbound) {
|
|
for (let rule in array_or_empty(config && config.route && config.route.rules)) {
|
|
if (type(rule) == "object" &&
|
|
rule.action == "route" &&
|
|
value_contains(rule.inbound, inbound) &&
|
|
value_contains(rule.outbound, outbound))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function runtime_config_status(sections) {
|
|
let config_path = option(uci_settings(), "config_path", "");
|
|
let config = read_json_file(config_path);
|
|
let rules_configured = length(sections) > 0;
|
|
let outbounds_configured = rules_configured;
|
|
let routes_configured = rules_configured;
|
|
|
|
let index_value = 1;
|
|
for (let section in sections) {
|
|
let outbound = runtime_tag(section_name(section), "out");
|
|
let port = rule_port(index_value);
|
|
if (!has_socks_outbound(config, outbound, BYEDPI_LISTEN_ADDRESS, port))
|
|
outbounds_configured = false;
|
|
if (!has_route_rule(config, SB_TPROXY_INBOUND_TAG, outbound))
|
|
routes_configured = false;
|
|
index_value++;
|
|
}
|
|
|
|
return {
|
|
rules_configured,
|
|
outbounds_configured,
|
|
routes_configured
|
|
};
|
|
}
|
|
|
|
function status_json() {
|
|
let sections = enabled_byedpi_sections();
|
|
let configured = length(sections) > 0;
|
|
let provider = provider_available();
|
|
let pkg = package_installed();
|
|
let version = pkg || provider ? package_version() : "not installed";
|
|
if (version == "")
|
|
version = "unknown";
|
|
|
|
let expected = length(sections);
|
|
let running = live_pid_count(BYEDPI_CHILD_PID_DIR);
|
|
let supervisors = live_pid_count(BYEDPI_PID_DIR);
|
|
let restarts = restart_count();
|
|
let standalone_enabled = standalone_service_enabled();
|
|
let standalone_running = standalone_service_running();
|
|
let config_state = runtime_config_status(sections);
|
|
let conflict = running > expected || standalone_running;
|
|
let unstable = configured && restarts > 0;
|
|
let ready = configured &&
|
|
provider &&
|
|
!standalone_running &&
|
|
!conflict &&
|
|
!unstable &&
|
|
config_state.outbounds_configured &&
|
|
config_state.routes_configured &&
|
|
expected > 0 &&
|
|
running == expected;
|
|
|
|
let message = "byedpi provider status is normal";
|
|
if (configured && !provider)
|
|
message = "action=byedpi is configured, but ciadpi is not available at " + BYEDPI_BIN;
|
|
else if (configured && standalone_running)
|
|
message = "standalone byedpi service is active together with Tachyon action=byedpi; port conflicts are possible";
|
|
else if (configured && standalone_enabled)
|
|
message = "standalone byedpi service autostart is enabled; disable it to avoid boot-time port conflicts with Tachyon action=byedpi";
|
|
else if (running > expected || supervisors > expected)
|
|
message = "unexpected Tachyon-managed ciadpi processes are running without matching action=byedpi rules";
|
|
else if (configured && unstable)
|
|
message = "Tachyon-managed ciadpi has restarted after exiting; the ByeDPI strategy or traffic load may be unstable";
|
|
else if (configured && !ready)
|
|
message = "action=byedpi is configured, but the Tachyon-managed ciadpi runtime is not ready";
|
|
else if (!configured && !provider && pkg)
|
|
message = "byedpi package is installed, but ciadpi is not available at " + BYEDPI_BIN;
|
|
else if (!configured && !provider)
|
|
message = "byedpi package is not installed; action=byedpi is unavailable";
|
|
|
|
write_json({
|
|
installed: provider,
|
|
package_installed: pkg,
|
|
provider_available: provider,
|
|
provider_path: BYEDPI_BIN,
|
|
version,
|
|
configured,
|
|
enabled_rule_count: expected,
|
|
expected_process_count: expected,
|
|
running_process_count: running,
|
|
supervisor_process_count: supervisors,
|
|
restart_count: restarts,
|
|
runtime_unstable: unstable,
|
|
standalone_service_enabled: standalone_enabled,
|
|
standalone_service_running: standalone_running,
|
|
listen_address: BYEDPI_LISTEN_ADDRESS,
|
|
port_base: int(BYEDPI_PORT_BASE),
|
|
outbounds_configured: config_state.outbounds_configured,
|
|
routes_configured: config_state.routes_configured,
|
|
ready,
|
|
conflict,
|
|
status_message: message
|
|
});
|
|
}
|
|
|
|
function check_json() {
|
|
write_json({
|
|
byedpi_installed: provider_available(),
|
|
byedpi_package_installed: package_installed(),
|
|
byedpi_provider_path: BYEDPI_BIN
|
|
});
|
|
}
|
|
|
|
let mode = ARGV[0] || "";
|
|
|
|
if (mode == "start-runtime")
|
|
start_runtime();
|
|
else if (mode == "stop-runtime")
|
|
stop_runtime();
|
|
else if (mode == "supervisor")
|
|
supervisor(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
|
|
else if (mode == "status")
|
|
status_json();
|
|
else if (mode == "check")
|
|
check_json();
|
|
else if (mode == "installed" || mode == "provider-available")
|
|
exit(provider_available() ? 0 : 1);
|
|
else if (mode == "package-installed")
|
|
exit(package_installed() ? 0 : 1);
|
|
else if (mode == "package-version")
|
|
print(package_version(), "\n");
|
|
else if (mode == "enabled-rule-count")
|
|
print(enabled_rule_count(), "\n");
|
|
else {
|
|
warn("Usage: providers/byedpi/runtime.uc <start-runtime|stop-runtime|status|check|installed|package-installed|package-version> ...\n");
|
|
exit(1);
|
|
} |