Files
NeoTachyon/tachyon/files/usr/lib/providers/byedpi/runtime.uc
T

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);
}