416 lines
15 KiB
Ucode
416 lines
15 KiB
Ucode
#!/usr/bin/env ucode
|
|
|
|
let fs = require("fs");
|
|
let common = require("core.common");
|
|
let uci_core = require("core.uci");
|
|
let connections = require("config.connections");
|
|
|
|
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_status = common.command_status;
|
|
let command_success_from_args = common.command_success_from_args;
|
|
let object_or_empty = common.object_or_empty;
|
|
let option = common.option;
|
|
let bool_option = common.bool_option;
|
|
let section_name = common.section_name;
|
|
let remove_file = common.remove_file;
|
|
|
|
const MODE = as_string(getenv("TACHYON_CONFIG_NAME")) || "tachyon";
|
|
const LIB_DIR = as_string(getenv("TACHYON_LIB_DIR")) || "/usr/lib/tachyon";
|
|
const CONFIG_NAME = MODE;
|
|
|
|
let cfg = require("providers.fptn.common").config({ lib_dir: LIB_DIR });
|
|
|
|
function log_message(msg, level) {
|
|
level = level || "info";
|
|
command_status("logger -t tachyon-fptn -p " + (level == "fatal" ? "user.err" : level == "warn" ? "user.warning" : "user.info") + " " + shell_quote(msg));
|
|
}
|
|
|
|
function provider_available() {
|
|
let st = fs.stat(cfg.binary);
|
|
return st != null && st.mode != null && (int(st.mode) & 73) != 0;
|
|
}
|
|
|
|
function enabled_sections() {
|
|
let sections = uci_core.section_objects(CONFIG_NAME, "section");
|
|
if (connections && connections.active_provider_sections)
|
|
return connections.active_provider_sections("fptn", sections);
|
|
if (connections && connections.fptn_sections)
|
|
return connections.fptn_sections();
|
|
|
|
let result = [];
|
|
for (let s in sections) {
|
|
if (bool_option(s, "enabled", true) && option(s, "action", "") == "fptn")
|
|
push(result, s);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function enabled_rule_count() {
|
|
return length(enabled_sections());
|
|
}
|
|
|
|
function running_pid() {
|
|
let pid_data = trim(as_string(fs.readfile(cfg.pid_file) || ""));
|
|
if (pid_data == "" || match(pid_data, /[^0-9]/))
|
|
return null;
|
|
|
|
let pid = int(pid_data);
|
|
if (pid <= 0)
|
|
return null;
|
|
|
|
if (fs.stat("/proc/" + pid) != null)
|
|
return pid;
|
|
|
|
remove_file(cfg.pid_file);
|
|
return null;
|
|
}
|
|
|
|
function package_version() {
|
|
if (!provider_available())
|
|
return "";
|
|
|
|
let out = trim(command_output(shell_quote(cfg.binary) + " --version 2>&1 || true"));
|
|
let m = match(out, /version[ \t]*([0-9a-zA-Z._-]+)/i) || match(out, /([0-9]+\.[0-9a-zA-Z._-]+)/);
|
|
if (m)
|
|
return m[1];
|
|
|
|
let pkgs = require("core.packages");
|
|
if (pkgs && pkgs.version)
|
|
return pkgs.version("fptn") || pkgs.version("fptn-client") || "";
|
|
|
|
return "";
|
|
}
|
|
|
|
function remove_kernel_routing() {
|
|
let p = cfg.rule_priority || "102";
|
|
let mark_spec = cfg.fwmark + "/" + cfg.mark_mask;
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
if (command_status("ip rule del fwmark " + mark_spec + " table " + cfg.route_table + " priority " + p + " 2>/dev/null") != 0)
|
|
break;
|
|
}
|
|
|
|
command_status("ip route flush table " + cfg.route_table + " 2>/dev/null; true");
|
|
command_status("ip -4 route del default dev " + shell_quote(cfg.tun_interface) + " table main 2>/dev/null; true");
|
|
if (command_status("ip link show " + shell_quote(cfg.tun_interface) + " >/dev/null 2>&1") == 0)
|
|
command_status("ip link set dev " + shell_quote(cfg.tun_interface) + " down 2>/dev/null; true");
|
|
}
|
|
|
|
function install_kernel_routing() {
|
|
remove_kernel_routing();
|
|
|
|
command_status("ip link set dev " + shell_quote(cfg.tun_interface) + " up 2>/dev/null; true");
|
|
let route_ok = command_status("ip route replace default dev " + shell_quote(cfg.tun_interface) + " table " + cfg.route_table + " 2>/dev/null") == 0;
|
|
if (!route_ok) {
|
|
log_message("Failed to add default route dev " + cfg.tun_interface + " to table " + cfg.route_table, "warn");
|
|
return false;
|
|
}
|
|
|
|
let p = cfg.rule_priority || "102";
|
|
let mark_spec = cfg.fwmark + "/" + cfg.mark_mask;
|
|
let rule_ok = command_status("ip rule add fwmark " + mark_spec + " table " + cfg.route_table + " priority " + p + " 2>/dev/null") == 0;
|
|
if (!rule_ok) {
|
|
log_message("Failed to add ip rule for table " + cfg.route_table, "warn");
|
|
return false;
|
|
}
|
|
|
|
// Ensure table main default route was not hijacked by fptn-client-cli
|
|
let main_default_dev = trim(command_output("ip -4 route show table main default 2>/dev/null | awk '{print $5; exit}'"));
|
|
if (main_default_dev == cfg.tun_interface) {
|
|
log_message("FPTN client hijacked table main default route; restoring WAN route", "warn");
|
|
command_status("ip -4 route del default dev " + shell_quote(cfg.tun_interface) + " table main 2>/dev/null; true");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function stop_runtime() {
|
|
let pid = running_pid();
|
|
if (pid != null) {
|
|
log_message("Stopping FPTN daemon (PID " + pid + ")", "info");
|
|
command_status("kill -15 " + pid + " 2>/dev/null; true");
|
|
|
|
let stopped = false;
|
|
for (let i = 0; i < 20; i++) {
|
|
if (fs.stat("/proc/" + pid) == null) {
|
|
stopped = true;
|
|
break;
|
|
}
|
|
system("sleep 0.1 2>/dev/null || sleep 1");
|
|
}
|
|
|
|
if (!stopped && fs.stat("/proc/" + pid) != null) {
|
|
log_message("FPTN daemon did not stop gracefully; sending SIGKILL", "warn");
|
|
command_status("kill -9 " + pid + " 2>/dev/null; true");
|
|
}
|
|
}
|
|
|
|
remove_file(cfg.pid_file);
|
|
remove_kernel_routing();
|
|
return true;
|
|
}
|
|
|
|
function launch_fptn_process(section) {
|
|
let token = as_string(option(section, "access_token", ""));
|
|
if (token == "")
|
|
return false;
|
|
|
|
command_status("mkdir -p " + shell_quote(cfg.state_dir) + " 2>/dev/null");
|
|
|
|
let cmd_args = [
|
|
cfg.binary,
|
|
"--access-token", token,
|
|
"--tun-interface-name", cfg.tun_interface,
|
|
"--enable-split-tunnel", "false"
|
|
];
|
|
|
|
let sni = as_string(option(section, "sni", ""));
|
|
if (sni != "") {
|
|
push(cmd_args, "--sni");
|
|
push(cmd_args, sni);
|
|
}
|
|
|
|
let bypass_method = as_string(option(section, "bypass_method", ""));
|
|
if (bypass_method != "") {
|
|
push(cmd_args, "--bypass-method");
|
|
push(cmd_args, bypass_method);
|
|
}
|
|
|
|
let preferred_server = as_string(option(section, "preferred_server", ""));
|
|
if (preferred_server != "") {
|
|
push(cmd_args, "--preferred-server");
|
|
push(cmd_args, preferred_server);
|
|
}
|
|
|
|
let cmd_str = command_from_args(cmd_args) + " >> " + shell_quote(cfg.log_file) + " 2>&1 & echo $! > " + shell_quote(cfg.pid_file);
|
|
log_message("Starting FPTN client on interface " + cfg.tun_interface, "info");
|
|
system(cmd_str);
|
|
return true;
|
|
}
|
|
|
|
function supervise_runtime() {
|
|
let sup_pid_file = cfg.state_dir + "/supervisor.pid";
|
|
let cur_pid = trim(as_string(fs.readfile(sup_pid_file) || ""));
|
|
if (cur_pid != "" && !match(cur_pid, /[^0-9]/) && fs.stat("/proc/" + cur_pid) != null)
|
|
return true;
|
|
|
|
let delays = [ 2, 5, 10, 20, 30, 30, 30 ];
|
|
for (let i = 0; i < length(delays); i++) {
|
|
let sec_wait = delays[i];
|
|
system(sprintf("sleep %d 2>/dev/null || sleep %d", sec_wait, sec_wait));
|
|
|
|
let sections = enabled_sections();
|
|
if (length(sections) == 0)
|
|
break;
|
|
|
|
let tun_ok = command_status("ip link show " + shell_quote(cfg.tun_interface) + " >/dev/null 2>&1") == 0;
|
|
let p = running_pid();
|
|
|
|
if (!tun_ok || p == null) {
|
|
log_message("FPTN supervisor: retrying start (attempt " + (i + 1) + ")", "info");
|
|
stop_runtime();
|
|
launch_fptn_process(sections[0]);
|
|
for (let wait_i = 0; wait_i < 20; wait_i++) {
|
|
if (command_status("ip link show " + shell_quote(cfg.tun_interface) + " >/dev/null 2>&1") == 0) {
|
|
tun_ok = true;
|
|
break;
|
|
}
|
|
system("sleep 0.2 2>/dev/null || sleep 1");
|
|
}
|
|
}
|
|
|
|
if (tun_ok) {
|
|
let routed = install_kernel_routing();
|
|
if (routed) {
|
|
log_message("FPTN supervisor: interface " + cfg.tun_interface + " is UP and routing installed", "info");
|
|
remove_file(sup_pid_file);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
remove_file(sup_pid_file);
|
|
return false;
|
|
}
|
|
|
|
function ensure_routing() {
|
|
let sections = enabled_sections();
|
|
if (length(sections) == 0)
|
|
return true;
|
|
let tun_up = command_status("ip link show " + shell_quote(cfg.tun_interface) + " >/dev/null 2>&1") == 0;
|
|
let p = running_pid();
|
|
if (p != null && tun_up) {
|
|
let route_installed = command_status("ip route show table " + cfg.route_table + " default dev " + shell_quote(cfg.tun_interface) + " 2>/dev/null | grep -q default") == 0;
|
|
let rule_installed = command_status("ip rule show 2>/dev/null | grep -q " + shell_quote(cfg.route_table)) == 0;
|
|
if (route_installed && rule_installed)
|
|
return true;
|
|
return install_kernel_routing();
|
|
}
|
|
return start_runtime();
|
|
}
|
|
|
|
function start_runtime() {
|
|
let sections = enabled_sections();
|
|
if (length(sections) == 0) {
|
|
stop_runtime();
|
|
return true;
|
|
}
|
|
|
|
if (!provider_available()) {
|
|
log_message("Cannot start FPTN: binary " + cfg.binary + " not found or not executable", "warn");
|
|
return false;
|
|
}
|
|
|
|
let section = sections[0];
|
|
let token = as_string(option(section, "access_token", ""));
|
|
if (token == "") {
|
|
log_message("Cannot start FPTN: access_token not configured for section " + section_name(section), "warn");
|
|
return false;
|
|
}
|
|
|
|
// Stop and disable conflicting standalone init.d service if present
|
|
command_status("/etc/init.d/fptn stop >/dev/null 2>&1 || true; /etc/init.d/fptn disable >/dev/null 2>&1 || true;");
|
|
|
|
stop_runtime();
|
|
|
|
if (!launch_fptn_process(section))
|
|
return false;
|
|
|
|
let started = false;
|
|
for (let i = 0; i < 30; i++) {
|
|
if (command_status("ip link show " + shell_quote(cfg.tun_interface) + " >/dev/null 2>&1") == 0) {
|
|
started = true;
|
|
break;
|
|
}
|
|
let p = running_pid();
|
|
if (p == null && i > 5)
|
|
break;
|
|
system("sleep 0.1 2>/dev/null || sleep 1");
|
|
}
|
|
|
|
if (!started) {
|
|
log_message("FPTN interface " + cfg.tun_interface + " did not come up immediately (WAN may be initializing); launching background retry supervisor", "warn");
|
|
let sup_pid_file = cfg.state_dir + "/supervisor.pid";
|
|
let sup_exec = sprintf("ucode -L %s %s supervise-runtime",
|
|
shell_quote(LIB_DIR), shell_quote(LIB_DIR + "/providers/fptn/runtime.uc"));
|
|
system(common.background_command_with_pid(sup_exec, ">/dev/null", ">" + shell_quote(sup_pid_file)));
|
|
return true;
|
|
}
|
|
|
|
let routing_ok = install_kernel_routing();
|
|
if (!routing_ok) {
|
|
log_message("FPTN interface " + cfg.tun_interface + " came up but routing failed; launching background supervisor", "warn");
|
|
let sup_pid_file = cfg.state_dir + "/supervisor.pid";
|
|
let sup_exec = sprintf("ucode -L %s %s supervise-runtime",
|
|
shell_quote(LIB_DIR), shell_quote(LIB_DIR + "/providers/fptn/runtime.uc"));
|
|
system(common.background_command_with_pid(sup_exec, ">/dev/null", ">" + shell_quote(sup_pid_file)));
|
|
return true;
|
|
}
|
|
|
|
log_message("FPTN client successfully started on " + cfg.tun_interface + " (table " + cfg.route_table + ")", "info");
|
|
return true;
|
|
}
|
|
|
|
function status_json() {
|
|
let installed = provider_available();
|
|
let pid = running_pid();
|
|
let running = pid != null;
|
|
let rule_count = enabled_rule_count();
|
|
let ver = package_version();
|
|
let tun_up = command_status("ip link show " + shell_quote(cfg.tun_interface) + " >/dev/null 2>&1") == 0;
|
|
let route_installed = command_status("ip route show table " + cfg.route_table + " default dev " + shell_quote(cfg.tun_interface) + " 2>/dev/null | grep -q default") == 0;
|
|
let rule_installed = command_status("ip rule show 2>/dev/null | grep -q " + shell_quote(cfg.route_table)) == 0;
|
|
let ready = installed && running && tun_up && route_installed && rule_installed && rule_count > 0;
|
|
|
|
let status_msg = "FPTN is running";
|
|
if (!installed)
|
|
status_msg = "FPTN is not installed";
|
|
else if (rule_count == 0)
|
|
status_msg = "FPTN is not configured";
|
|
else if (!running)
|
|
status_msg = "FPTN is stopped";
|
|
else if (!tun_up)
|
|
status_msg = "FPTN is running (degraded: tun interface down)";
|
|
else if (!route_installed)
|
|
status_msg = "FPTN is running (degraded: table " + cfg.route_table + " route missing)";
|
|
else if (!rule_installed)
|
|
status_msg = "FPTN is running (degraded: table " + cfg.route_table + " ip rule missing)";
|
|
|
|
write_json({
|
|
installed: installed,
|
|
configured: rule_count > 0,
|
|
enabled_rule_count: rule_count,
|
|
service_running: running,
|
|
process_running: running,
|
|
tun_up: tun_up,
|
|
route_installed: route_installed,
|
|
rule_installed: rule_installed,
|
|
pid: pid,
|
|
version: ver,
|
|
binary: cfg.binary,
|
|
tun_interface: cfg.tun_interface,
|
|
route_table: cfg.route_table,
|
|
log_file: cfg.log_file,
|
|
ready: ready,
|
|
status_message: status_msg
|
|
});
|
|
return true;
|
|
}
|
|
|
|
function check_json() {
|
|
write_json({
|
|
fptn_installed: provider_available() ? 1 : 0,
|
|
fptn_version: package_version(),
|
|
binary: cfg.binary
|
|
});
|
|
return true;
|
|
}
|
|
|
|
function module_exports() {
|
|
return {
|
|
provider_available: provider_available,
|
|
package_version: package_version,
|
|
start_runtime: start_runtime,
|
|
stop_runtime: stop_runtime,
|
|
ensure_routing: ensure_routing,
|
|
supervise_runtime: supervise_runtime,
|
|
status_json: status_json,
|
|
check_json: check_json
|
|
};
|
|
}
|
|
|
|
if (sourcepath(1) != null && sourcepath(1) != "")
|
|
return module_exports();
|
|
|
|
let mode = as_string(ARGV[0]);
|
|
if (mode == "start-runtime")
|
|
exit(start_runtime() ? 0 : 1);
|
|
else if (mode == "stop-runtime")
|
|
exit(stop_runtime() ? 0 : 1);
|
|
else if (mode == "restart-runtime") {
|
|
stop_runtime();
|
|
exit(start_runtime() ? 0 : 1);
|
|
}
|
|
else if (mode == "ensure-routing")
|
|
exit(ensure_routing() ? 0 : 1);
|
|
else if (mode == "supervise-runtime")
|
|
exit(supervise_runtime() ? 0 : 1);
|
|
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-version" || mode == "version")
|
|
printf("%s\n", package_version());
|
|
else if (mode == "enabled-rule-count")
|
|
printf("%d\n", enabled_rule_count());
|
|
else {
|
|
warn("Usage: providers/fptn/runtime.uc <start-runtime|stop-runtime|restart-runtime|ensure-routing|supervise-runtime|status|check|installed|version>\n");
|
|
exit(1);
|
|
}
|