feat(fptn): integrate FPTN client component and runtime provider
This commit is contained in:
@@ -1417,6 +1417,43 @@ function resolve_olcrtc_release(arch, tag) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolve_fptn_release(arch, tag) {
|
||||
let asset_ext = is_apk() ? "apk" : "ipk";
|
||||
let release_series = trim(helper_output("openwrt-release-series", [ "/etc/openwrt_release" ]));
|
||||
let releases_json = (tag != null && tag != "") ?
|
||||
fetch_github_release_by_tag_json("fptn-project", "fptn", tag) :
|
||||
fetch_github_releases_json("fptn-project", "fptn", "30");
|
||||
if (releases_json != "") {
|
||||
let resolved = trim(helper_output_input(releases_json, "fptn-select-asset", [ release_series, asset_ext, arch.candidates ]));
|
||||
let fields = split(resolved, "\t");
|
||||
if (length(fields) >= 4) {
|
||||
let ver = extract_arch_package_version(fields[1], fields[0]);
|
||||
let m = match(ver, /^([0-9]+\.[0-9]+\.[0-9]+)/);
|
||||
if (m) ver = m[1];
|
||||
return {
|
||||
arch: fields[0],
|
||||
package_name: fields[1],
|
||||
package_url: fields[2],
|
||||
release_url: fields[3],
|
||||
version: ver
|
||||
};
|
||||
}
|
||||
}
|
||||
if (tag != null && tag != "") {
|
||||
let distrib_arch = read_openwrt_release_value("DISTRIB_ARCH");
|
||||
let tag_clean = replace(tag, /^v/, "");
|
||||
let pkg_name = "fptn-client-" + tag_clean + "-openwrt-" + (release_series != "" ? release_series + ".x" : "24.10.x") + "-" + distrib_arch + "." + asset_ext;
|
||||
return {
|
||||
arch: distrib_arch,
|
||||
package_name: pkg_name,
|
||||
package_url: "https://github.com/fptn-project/fptn/releases/download/" + tag + "/" + pkg_name,
|
||||
release_url: "https://github.com/fptn-project/fptn/releases/tag/" + tag,
|
||||
version: tag_clean
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function download_direct_package(release) {
|
||||
let package_file = tmp_dir + "/" + release.package_name;
|
||||
if (!download_with_retry(release.package_url, package_file, release.package_name) || !file_nonempty(package_file))
|
||||
@@ -1688,6 +1725,47 @@ function install_olcrtc(action, target_tag) {
|
||||
action_success("olcrtc", action, "OlcRTC package has been installed", current_version, pkg.version, 1, "latest", release.release_url || "");
|
||||
}
|
||||
|
||||
function install_fptn(action, target_tag) {
|
||||
init_tmp_dir() || action_fail("fptn", action, "Failed to create temporary directory");
|
||||
let arch = resolve_arch_candidates();
|
||||
if (arch == null)
|
||||
action_fail("fptn", action, "Failed to detect package architecture");
|
||||
let release = null;
|
||||
retry_resolve("Resolving FPTN package", function() {
|
||||
release = resolve_fptn_release(arch, target_tag);
|
||||
return release != null;
|
||||
});
|
||||
if (release == null)
|
||||
action_fail("fptn", action, "Failed to resolve FPTN package for this router architecture");
|
||||
|
||||
let runtime_module = LIB_DIR + "/providers/fptn/runtime.uc";
|
||||
let installed = provider_installed(runtime_module);
|
||||
let current_version = provider_package_version(runtime_module);
|
||||
if (action == "check_update") {
|
||||
if (!installed)
|
||||
action_success("fptn", action, "FPTN is not installed", current_version, release.version, 0, "", release.release_url || "");
|
||||
check_success("fptn", current_version, release.version, release.release_url || "");
|
||||
}
|
||||
|
||||
let pkg = download_direct_package(release);
|
||||
if (pkg == null)
|
||||
action_fail("fptn", action, "Failed to download FPTN package");
|
||||
|
||||
run_logged("Updating package lists before FPTN package installation", pkg_list_update_command());
|
||||
|
||||
if (!run_logged("Installing FPTN package " + pkg.name, pkg_install_files_command([ pkg.file ])))
|
||||
action_fail("fptn", action, "Failed to install FPTN package", current_version, pkg.version);
|
||||
|
||||
disable_standalone_service("fptn");
|
||||
disable_standalone_service("fptn-client");
|
||||
restart_tachyon_after_successful_change();
|
||||
clear_version_caches();
|
||||
current_version = provider_package_version(runtime_module);
|
||||
if (current_version == "")
|
||||
current_version = pkg.version || "unknown";
|
||||
action_success("fptn", action, "FPTN package has been installed", current_version, pkg.version, 1, "latest", release.release_url || "");
|
||||
}
|
||||
|
||||
function install_tailscale(action) {
|
||||
let component = "tailscale";
|
||||
let label = "Tailscale";
|
||||
@@ -3071,6 +3149,8 @@ function normalize_component_name(component) {
|
||||
component = as_string(component);
|
||||
if (component == "sing-box" || component == "singbox")
|
||||
return "sing_box";
|
||||
if (component == "fptn-client" || component == "fptn_client")
|
||||
return "fptn";
|
||||
if (component == "tachyon")
|
||||
return "tachyon";
|
||||
if (component == "direct-bypass" || component == "directbypass" || component == "direct_proxy")
|
||||
@@ -3236,6 +3316,24 @@ function create_component_backup(component) {
|
||||
write_file(meta_file, sprintf("%J\n", meta));
|
||||
return true;
|
||||
}
|
||||
else if (component == "fptn") {
|
||||
let bin = "/usr/bin/fptn-client-cli";
|
||||
if (!file_exists(bin)) bin = "/usr/bin/fptn-client";
|
||||
if (!file_exists(bin)) return true;
|
||||
let st = fs.stat(bin);
|
||||
let size = (st && st.size) ? st.size : 0;
|
||||
if (!check_free_disk_space("/etc", size)) return false;
|
||||
ensure_dir(bdir);
|
||||
command_success_from_args([ "cp", "-p", bin, bdir + "/fptn-client-cli" ]);
|
||||
let version = provider_package_version(LIB_DIR + "/providers/fptn/runtime.uc");
|
||||
let meta = {
|
||||
component: "fptn",
|
||||
version: version,
|
||||
timestamp: now_seconds()
|
||||
};
|
||||
write_file(meta_file, sprintf("%J\n", meta));
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -3314,6 +3412,16 @@ function rollback_component(component) {
|
||||
restart_tachyon_after_successful_change();
|
||||
action_success("olcrtc", "rollback", "OlcRTC rolled back to " + backup_version, backup_version, "", 1);
|
||||
}
|
||||
else if (component == "fptn") {
|
||||
let backup_bin = bdir + "/fptn-client-cli";
|
||||
if (!file_exists(backup_bin)) action_fail("fptn", "rollback", "FPTN backup binary is missing");
|
||||
remove_file("/usr/bin/fptn-client-cli");
|
||||
command_success_from_args([ "cp", "-p", backup_bin, "/usr/bin/fptn-client-cli" ]);
|
||||
command_success_from_args([ "chmod", "0755", "/usr/bin/fptn-client-cli" ]);
|
||||
clear_version_caches();
|
||||
restart_tachyon_after_successful_change();
|
||||
action_success("fptn", "rollback", "FPTN rolled back to " + backup_version, backup_version, "", 1);
|
||||
}
|
||||
else if (component == "zapret") {
|
||||
let backup_bin = bdir + "/nfqws";
|
||||
if (!file_exists(backup_bin)) action_fail("zapret", "rollback", "Zapret backup binary is missing");
|
||||
@@ -3393,6 +3501,8 @@ function list_component_releases(component, count) {
|
||||
owner = "SpaceNeuroX"; repo = "qwdtt-openwrt";
|
||||
} else if (component == "olcrtc") {
|
||||
owner = "alekvol"; repo = "openwrt-olcrtc";
|
||||
} else if (component == "fptn") {
|
||||
owner = "fptn-project"; repo = "fptn";
|
||||
} else {
|
||||
print("[]\n"); return;
|
||||
}
|
||||
@@ -3449,6 +3559,8 @@ function install_component_version(component, tag) {
|
||||
install_wdtt("install", tag);
|
||||
} else if (component == "olcrtc") {
|
||||
install_olcrtc("install", tag);
|
||||
} else if (component == "fptn") {
|
||||
install_fptn("install", tag);
|
||||
} else {
|
||||
action_fail(component, "install_version", "Component " + component + " does not support version installation");
|
||||
}
|
||||
@@ -3595,6 +3707,10 @@ function component_action(component, action, extra) {
|
||||
install_olcrtc(action);
|
||||
else if (component == "olcrtc" && action == "remove")
|
||||
remove_optional_component("olcrtc", "olcrtc", "OlcRTC", LIB_DIR + "/providers/olcrtc/runtime.uc");
|
||||
else if (component == "fptn" && (action == "check_update" || action == "install"))
|
||||
install_fptn(action);
|
||||
else if (component == "fptn" && action == "remove")
|
||||
remove_optional_component("fptn", "fptn-client", "FPTN", LIB_DIR + "/providers/fptn/runtime.uc");
|
||||
else if (component == "tailscale" && (action == "check_update" || action == "install"))
|
||||
install_tailscale(action);
|
||||
else if (component == "tailscale" && action == "remove")
|
||||
|
||||
@@ -388,7 +388,7 @@ function openwrt_release_series(path) {
|
||||
|
||||
function updates_arch_package_version(package_name, package_arch) {
|
||||
let version = str_remove_suffix(str_remove_suffix(package_name, ".ipk"), ".apk");
|
||||
let prefixes = ["zapret2_", "zapret2-", "zapret_", "zapret-", "byedpi_", "byedpi-", "wdtt_", "wdtt-", "olcrtc_", "olcrtc-"];
|
||||
let prefixes = ["zapret2_", "zapret2-", "zapret_", "zapret-", "byedpi_", "byedpi-", "fptn-client-", "fptn-client_", "fptn_", "fptn-", "wdtt_", "wdtt-", "olcrtc_", "olcrtc-"];
|
||||
|
||||
for (let prefix in prefixes) {
|
||||
if (str_startswith(version, prefix)) {
|
||||
@@ -1011,6 +1011,50 @@ function byedpi_select_asset(series, asset_ext, arch_candidates) {
|
||||
}
|
||||
}
|
||||
|
||||
function fptn_asset_matches(name, series, arch, ext) {
|
||||
if (!str_startswith(name, "fptn-client-") && !str_startswith(name, "fptn-"))
|
||||
return false;
|
||||
if (!str_endswith(name, "." + ext))
|
||||
return false;
|
||||
if (series != "" && !str_contains(name, series))
|
||||
return false;
|
||||
return str_contains(name, "-" + arch + "." + ext) || str_contains(name, "_" + arch + "." + ext);
|
||||
}
|
||||
|
||||
function select_fptn_asset_from_release(release, series, asset_ext, arch_candidates) {
|
||||
for (let arch in split(as_string(arch_candidates), " ")) {
|
||||
if (arch == "")
|
||||
continue;
|
||||
for (let asset in array_or_empty(release.assets)) {
|
||||
if (type(asset) != "object")
|
||||
continue;
|
||||
let name = as_string(asset.name || "");
|
||||
let url = as_string(asset.browser_download_url || "");
|
||||
if (url != "" && fptn_asset_matches(name, series, arch, asset_ext)) {
|
||||
print(arch, "\t", name, "\t", url, "\t", as_string(release.html_url || ""), "\n");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function fptn_select_asset(series, asset_ext, arch_candidates) {
|
||||
let releases = releases_array_or_wrapped(read_stdin_json());
|
||||
|
||||
for (let pass = 0; pass < 2; pass++) {
|
||||
let current_series = pass == 0 ? as_string(series) : "";
|
||||
for (let release in releases) {
|
||||
if (type(release) != "object")
|
||||
continue;
|
||||
if (release.draft === true || (length(releases) > 1 && release.prerelease === true))
|
||||
continue;
|
||||
if (select_fptn_asset_from_release(release, current_series, asset_ext, arch_candidates))
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function wdtt_select_asset(series, asset_ext, arch_candidates) {
|
||||
let releases = releases_array_or_wrapped(read_stdin_json());
|
||||
|
||||
@@ -1494,6 +1538,8 @@ else if (mode == "wdtt-select-asset")
|
||||
wdtt_select_asset(ARGV[1], ARGV[2], ARGV[3]);
|
||||
else if (mode == "olcrtc-select-asset")
|
||||
olcrtc_select_asset(ARGV[1], ARGV[2], ARGV[3]);
|
||||
else if (mode == "fptn-select-asset")
|
||||
fptn_select_asset(ARGV[1], ARGV[2], ARGV[3]);
|
||||
else if (mode == "sing-box-extended-release-tag")
|
||||
sing_box_extended_release_tag();
|
||||
else if (mode == "sing-box-lx-release-tag")
|
||||
|
||||
@@ -622,7 +622,7 @@ function normalize_component_name(component) {
|
||||
function valid_component_name(component) {
|
||||
component = normalize_component_name(component);
|
||||
return component == "tachyon" || component == "sing_box" || component == "zapret" ||
|
||||
component == "zapret2" || component == "byedpi" || component == "wdtt" || component == "olcrtc" || component == "tailscale";
|
||||
component == "zapret2" || component == "byedpi" || component == "wdtt" || component == "olcrtc" || component == "fptn" || component == "tailscale";
|
||||
}
|
||||
|
||||
function settings_component_auto_update_enabled(settings) {
|
||||
@@ -1601,7 +1601,7 @@ function component_update_check_cache() {
|
||||
let results = [];
|
||||
|
||||
if (enabled) {
|
||||
for (let component in [ "tachyon", "sing_box", "zapret", "zapret2", "byedpi", "wdtt", "olcrtc" ]) {
|
||||
for (let component in [ "tachyon", "sing_box", "zapret", "zapret2", "byedpi", "wdtt", "olcrtc", "fptn" ]) {
|
||||
let value = read_json_file(component_update_check_cache_path(component));
|
||||
if (component_update_check_result_cacheable(value))
|
||||
push(results, value);
|
||||
@@ -2031,6 +2031,8 @@ function automatic_component_check_names() {
|
||||
push(result, "wdtt");
|
||||
if (module_success([ LIB_DIR + "/providers/olcrtc/runtime.uc", "installed" ]))
|
||||
push(result, "olcrtc");
|
||||
if (module_success([ LIB_DIR + "/providers/fptn/runtime.uc", "installed" ]))
|
||||
push(result, "fptn");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -340,6 +340,10 @@ function is_olcrtc_action(action) {
|
||||
return as_string(action) == "olcrtc";
|
||||
}
|
||||
|
||||
function is_fptn_action(action) {
|
||||
return as_string(action) == "fptn";
|
||||
}
|
||||
|
||||
function wdtt_sections() {
|
||||
let result = [];
|
||||
let sections = uci_core.section_objects(CONFIG_NAME, "section");
|
||||
@@ -360,6 +364,16 @@ function olcrtc_sections() {
|
||||
return result;
|
||||
}
|
||||
|
||||
function fptn_sections() {
|
||||
let result = [];
|
||||
let sections = uci_core.section_objects(CONFIG_NAME, "section");
|
||||
for (let section in sections) {
|
||||
if (bool_option(section, "enabled", true) && option(section, "action", "") == "fptn")
|
||||
push(result, section);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function normalize_action(action) {
|
||||
action = as_string(action);
|
||||
return is_connections_action(action) ? "connection" : action;
|
||||
@@ -1634,6 +1648,8 @@ return {
|
||||
cli_delete_section,
|
||||
wdtt_sections,
|
||||
olcrtc_sections,
|
||||
fptn_sections,
|
||||
is_wdtt_action,
|
||||
is_olcrtc_action
|
||||
is_olcrtc_action,
|
||||
is_fptn_action
|
||||
};
|
||||
|
||||
@@ -27,6 +27,8 @@ function binary_installed(package_name) {
|
||||
return fs.stat("/opt/zapret/nfq/nfqws") != null || fs.stat("/opt/zapret/nfqws") != null;
|
||||
if (package_name == "byedpi")
|
||||
return fs.stat("/usr/bin/ciadpi") != null;
|
||||
if (package_name == "fptn" || package_name == "fptn-client")
|
||||
return fs.stat("/usr/bin/fptn-client-cli") != null || fs.stat("/usr/bin/fptn-client") != null;
|
||||
if (package_name == "sing-box" || package_name == "sing-box-extended" || package_name == "sing-box-lx")
|
||||
return fs.stat("/usr/bin/sing-box") != null;
|
||||
return false;
|
||||
@@ -151,6 +153,15 @@ function binary_version(package_name) {
|
||||
let m = match(out, /([0-9a-zA-Z._-]+)/);
|
||||
if (m) return m[1];
|
||||
}
|
||||
if ((package_name == "fptn" || package_name == "fptn-client")) {
|
||||
for (let p in [ "/usr/bin/fptn-client-cli", "/usr/bin/fptn-client" ]) {
|
||||
if (fs.stat(p) != null) {
|
||||
let out = command_output_args([ p, "--version" ]);
|
||||
let m = match(out, /version[ \t]*([0-9a-zA-Z._-]+)/i) || match(out, /([0-9]+\.[0-9a-zA-Z._-]+)/);
|
||||
if (m) return m[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((package_name == "sing-box" || package_name == "sing-box-extended" || package_name == "sing-box-lx") && fs.stat("/usr/bin/sing-box") != null) {
|
||||
let out = command_output_args([ "/usr/bin/sing-box", "version" ]);
|
||||
let m = match(out, /version[ \t]*([0-9a-zA-Z._-]+)/i);
|
||||
|
||||
@@ -59,6 +59,7 @@ const ZAPRET2_RUNTIME_UC = LIB_DIR + "/providers/zapret2/runtime.uc";
|
||||
const BYEDPI_RUNTIME_UC = LIB_DIR + "/providers/byedpi/runtime.uc";
|
||||
const WDTT_RUNTIME_UC = LIB_DIR + "/providers/wdtt/runtime.uc";
|
||||
const OLCRTC_RUNTIME_UC = LIB_DIR + "/providers/olcrtc/runtime.uc";
|
||||
const FPTN_RUNTIME_UC = LIB_DIR + "/providers/fptn/runtime.uc";
|
||||
const TAILSCALE_RUNTIME_UC = LIB_DIR + "/providers/tailscale/runtime.uc";
|
||||
const ZAPRET_VALIDATOR_UC = LIB_DIR + "/providers/zapret/validator.uc";
|
||||
const ZAPRET2_VALIDATOR_UC = LIB_DIR + "/providers/zapret2/validator.uc";
|
||||
@@ -1148,6 +1149,8 @@ function build_system_info() {
|
||||
let byedpi_version = byedpi_installed ? provider_version(BYEDPI_RUNTIME_UC) : "not installed";
|
||||
let tailscale_installed = provider_installed(TAILSCALE_RUNTIME_UC) ? 1 : 0;
|
||||
let tailscale_version = tailscale_installed ? provider_version(TAILSCALE_RUNTIME_UC) : "not installed";
|
||||
let fptn_installed = provider_installed(FPTN_RUNTIME_UC) ? 1 : 0;
|
||||
let fptn_version = fptn_installed ? provider_version(FPTN_RUNTIME_UC) : "not installed";
|
||||
let device_model = first_line_value("/tmp/sysinfo/model", "unknown");
|
||||
|
||||
let direct_bypass_enabled = bool_option(settings(), "direct_bypass_enabled", false) ? 1 : 0;
|
||||
@@ -1177,6 +1180,7 @@ function build_system_info() {
|
||||
let zapret2_meta = read_backup_meta("zapret2");
|
||||
let byedpi_meta = read_backup_meta("byedpi");
|
||||
let tailscale_meta = read_backup_meta("tailscale");
|
||||
let fptn_meta = read_backup_meta("fptn");
|
||||
|
||||
return {
|
||||
tachyon_version: TACHYON_VERSION,
|
||||
@@ -1208,6 +1212,10 @@ function build_system_info() {
|
||||
tailscale_installed,
|
||||
tailscale_backup_version: tailscale_meta ? as_string(tailscale_meta.version) : "",
|
||||
tailscale_backup_time: tailscale_meta ? int(tailscale_meta.timestamp || 0) : 0,
|
||||
fptn_version,
|
||||
fptn_installed,
|
||||
fptn_backup_version: fptn_meta ? as_string(fptn_meta.version) : "",
|
||||
fptn_backup_time: fptn_meta ? int(fptn_meta.timestamp || 0) : 0,
|
||||
direct_bypass_enabled,
|
||||
direct_bypass_address,
|
||||
direct_bypass_port,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
let fs = require("fs");
|
||||
|
||||
function config(ctx) {
|
||||
let env_get = function(key, fallback) {
|
||||
let val = getenv(key);
|
||||
return val != null ? val : fallback;
|
||||
};
|
||||
|
||||
let bin = env_get("TACHYON_FPTN_BIN", "/usr/bin/fptn-client-cli");
|
||||
if (fs.stat(bin) == null && fs.stat("/usr/bin/fptn-client") != null)
|
||||
bin = "/usr/bin/fptn-client";
|
||||
|
||||
let state_dir = env_get("TACHYON_FPTN_STATE_DIR", "/var/run/tachyon/fptn");
|
||||
|
||||
return {
|
||||
kind: "fptn",
|
||||
action: "fptn",
|
||||
binary: bin,
|
||||
tun_interface: env_get("TACHYON_FPTN_TUN", "tun-fptn"),
|
||||
route_table: env_get("TACHYON_FPTN_ROUTE_TABLE", "4249"),
|
||||
fwmark: env_get("TACHYON_FPTN_FWMARK", "0x00300000"),
|
||||
mark_mask: env_get("TACHYON_FPTN_MARK_MASK", "0x00ff0000"),
|
||||
rule_priority: env_get("TACHYON_FPTN_RULE_PRIORITY", "102"),
|
||||
state_dir: state_dir,
|
||||
pid_file: state_dir + "/fptn.pid",
|
||||
log_file: state_dir + "/fptn.log",
|
||||
package_name: "fptn",
|
||||
runtime_path: (ctx && ctx.lib_dir ? ctx.lib_dir : "/usr/lib/tachyon") + "/providers/fptn/runtime.uc",
|
||||
config_name: "fptn",
|
||||
status_label: "fptn",
|
||||
check_prefix: "fptn"
|
||||
};
|
||||
}
|
||||
|
||||
function validator() {
|
||||
return require("providers.fptn.validator");
|
||||
}
|
||||
|
||||
return { config, validator };
|
||||
@@ -0,0 +1,291 @@
|
||||
#!/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() {
|
||||
if (connections && connections.fptn_sections)
|
||||
return connections.fptn_sections();
|
||||
|
||||
let result = [];
|
||||
let sections = uci_core.section_objects(CONFIG_NAME, "section");
|
||||
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");
|
||||
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;
|
||||
}
|
||||
|
||||
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 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_runtime();
|
||||
|
||||
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);
|
||||
|
||||
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", "warn");
|
||||
return false;
|
||||
}
|
||||
|
||||
install_kernel_routing();
|
||||
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();
|
||||
|
||||
write_json({
|
||||
installed: installed,
|
||||
configured: rule_count > 0,
|
||||
enabled_rule_count: rule_count,
|
||||
service_running: running,
|
||||
pid: pid,
|
||||
version: ver,
|
||||
binary: cfg.binary,
|
||||
tun_interface: cfg.tun_interface,
|
||||
route_table: cfg.route_table,
|
||||
log_file: cfg.log_file,
|
||||
ready: installed && running && rule_count > 0,
|
||||
status_message: running ? "FPTN is running" : (installed ? "FPTN is installed but not running" : "FPTN is not installed")
|
||||
});
|
||||
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,
|
||||
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 == "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|status|check|installed|version>\n");
|
||||
exit(1);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
let common = require("core.common");
|
||||
let as_string = common.as_string;
|
||||
|
||||
function is_valid_token(token) {
|
||||
token = trim(as_string(token));
|
||||
return length(token) > 0;
|
||||
}
|
||||
|
||||
function validate_section(section) {
|
||||
if (!section)
|
||||
return { valid: false, error: "Section is missing" };
|
||||
|
||||
let token = as_string(section.access_token || "");
|
||||
if (!is_valid_token(token))
|
||||
return { valid: false, error: "Missing or invalid FPTN access token" };
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
return {
|
||||
is_valid_token,
|
||||
validate_section
|
||||
};
|
||||
@@ -122,6 +122,7 @@ const BYEDPI_UC = LIB_DIR + "/providers/byedpi/runtime.uc";
|
||||
const TAILSCALE_UC = LIB_DIR + "/providers/tailscale/runtime.uc";
|
||||
const WDTT_UC = LIB_DIR + "/providers/wdtt/runtime.uc";
|
||||
const OLCRTC_UC = LIB_DIR + "/providers/olcrtc/runtime.uc";
|
||||
const FPTN_UC = LIB_DIR + "/providers/fptn/runtime.uc";
|
||||
const PARENTAL_QUOTA_UC = LIB_DIR + "/service/parental_quota.uc";
|
||||
const PACKAGES_UC = LIB_DIR + "/core/packages.uc";
|
||||
const WATCHDOG_UC = LIB_DIR + "/service/watchdog.uc";
|
||||
@@ -943,6 +944,7 @@ function start_main() {
|
||||
module_success(ZAPRET2_UC, [ "start-runtime" ]);
|
||||
module_success(WDTT_UC, [ "start-runtime" ]);
|
||||
module_success(OLCRTC_UC, [ "start-runtime" ]);
|
||||
module_success(FPTN_UC, [ "start-runtime" ]);
|
||||
module_success(PARENTAL_QUOTA_UC, [ "install-cron" ]);
|
||||
module_success(PARENTAL_QUOTA_UC, [ "tick" ]);
|
||||
|
||||
@@ -1036,6 +1038,8 @@ function stop_main() {
|
||||
log_message("WDTT stop failed (non-fatal)", "warn");
|
||||
if (!module_success(OLCRTC_UC, [ "stop-runtime" ]))
|
||||
log_message("OlcRTC stop failed (non-fatal)", "warn");
|
||||
if (!module_success(FPTN_UC, [ "stop-runtime" ]))
|
||||
log_message("FPTN stop failed (non-fatal)", "warn");
|
||||
if (!module_success(TAILSCALE_UC, [ "stop-runtime" ]))
|
||||
log_message("Tailscale stop failed (non-fatal)", "warn");
|
||||
module_success(PARENTAL_QUOTA_UC, [ "remove-cron" ]);
|
||||
@@ -1230,6 +1234,7 @@ function parse_reload_plan(output) {
|
||||
needs_byedpi_restart: 0,
|
||||
needs_wdtt_restart: 0,
|
||||
needs_olcrtc_restart: 0,
|
||||
needs_fptn_restart: 0,
|
||||
needs_dnsmasq_configure: 0,
|
||||
needs_dnsmasq_restore: 0,
|
||||
needs_cron_refresh: 0,
|
||||
@@ -1268,6 +1273,7 @@ function reload_actions_summary(plan) {
|
||||
actions = append_reload_action(actions, plan.needs_byedpi_restart, "ByeDPI");
|
||||
actions = append_reload_action(actions, plan.needs_wdtt_restart, "WDTT");
|
||||
actions = append_reload_action(actions, plan.needs_olcrtc_restart, "OlcRTC");
|
||||
actions = append_reload_action(actions, plan.needs_fptn_restart, "FPTN");
|
||||
actions = append_reload_action(actions, plan.needs_dnsmasq_configure || plan.needs_dnsmasq_restore, "dnsmasq");
|
||||
actions = append_reload_action(actions, plan.needs_cron_refresh, "scheduled jobs");
|
||||
actions = append_reload_action(actions, plan.needs_list_update, "remote lists");
|
||||
@@ -1462,6 +1468,8 @@ function reload(reason) {
|
||||
module_success(WDTT_UC, [ "stop-runtime" ]);
|
||||
if (plan.needs_olcrtc_restart == 1)
|
||||
module_success(OLCRTC_UC, [ "stop-runtime" ]);
|
||||
if (plan.needs_fptn_restart == 1)
|
||||
module_success(FPTN_UC, [ "stop-runtime" ]);
|
||||
|
||||
if (plan.needs_hosts_update == 1 && module_success(STATE_UC, [ "has-hosts-list-update-sources" ]))
|
||||
module_success(HOSTS_UC, [ "list-update" ]);
|
||||
@@ -1534,6 +1542,8 @@ function reload(reason) {
|
||||
module_success(WDTT_UC, [ "start-runtime" ]);
|
||||
if (plan.needs_olcrtc_restart == 1)
|
||||
module_success(OLCRTC_UC, [ "start-runtime" ]);
|
||||
if (plan.needs_fptn_restart == 1)
|
||||
module_success(FPTN_UC, [ "start-runtime" ]);
|
||||
if (plan.needs_byedpi_restart == 1)
|
||||
module_success(BYEDPI_UC, [ "start-runtime" ]);
|
||||
// Native Tailscale must be up before sing-box so tailnet routes win over
|
||||
|
||||
@@ -65,6 +65,7 @@ function emit_reload_plan(previous, current, context) {
|
||||
byedpi_runtime: current.byedpi_runtime != previous.byedpi_runtime,
|
||||
wdtt_runtime: current.wdtt_runtime != previous.wdtt_runtime,
|
||||
olcrtc_runtime: current.olcrtc_runtime != previous.olcrtc_runtime,
|
||||
fptn_runtime: current.fptn_runtime != previous.fptn_runtime,
|
||||
list: current.list != previous.list,
|
||||
hosts_list: current.hosts_list != previous.hosts_list,
|
||||
cron: current.cron != previous.cron
|
||||
@@ -78,6 +79,7 @@ function emit_reload_plan(previous, current, context) {
|
||||
byedpi_restart: false,
|
||||
wdtt_restart: false,
|
||||
olcrtc_restart: false,
|
||||
fptn_restart: false,
|
||||
dnsmasq_configure: false,
|
||||
dnsmasq_restore: false,
|
||||
cron_refresh: false,
|
||||
@@ -122,6 +124,9 @@ function emit_reload_plan(previous, current, context) {
|
||||
if (changed.olcrtc_runtime)
|
||||
needs.olcrtc_restart = true;
|
||||
|
||||
if (changed.fptn_runtime)
|
||||
needs.fptn_restart = true;
|
||||
|
||||
if (changed.dnsmasq) {
|
||||
if (!current.dont_touch_dhcp)
|
||||
needs.dnsmasq_configure = true;
|
||||
@@ -187,6 +192,7 @@ function emit_reload_plan(previous, current, context) {
|
||||
emit_bool("changed_byedpi_runtime", changed.byedpi_runtime);
|
||||
emit_bool("changed_wdtt_runtime", changed.wdtt_runtime);
|
||||
emit_bool("changed_olcrtc_runtime", changed.olcrtc_runtime);
|
||||
emit_bool("changed_fptn_runtime", changed.fptn_runtime);
|
||||
emit_bool("changed_cron", changed.cron);
|
||||
emit_bool("changed_list", changed.list);
|
||||
emit_bool("changed_hosts_list", changed.hosts_list);
|
||||
@@ -198,6 +204,7 @@ function emit_reload_plan(previous, current, context) {
|
||||
emit_bool("needs_byedpi_restart", needs.byedpi_restart);
|
||||
emit_bool("needs_wdtt_restart", needs.wdtt_restart);
|
||||
emit_bool("needs_olcrtc_restart", needs.olcrtc_restart);
|
||||
emit_bool("needs_fptn_restart", needs.fptn_restart);
|
||||
emit_bool("needs_dnsmasq_configure", needs.dnsmasq_configure);
|
||||
emit_bool("needs_dnsmasq_restore", needs.dnsmasq_restore);
|
||||
emit_bool("needs_cron_refresh", needs.cron_refresh);
|
||||
@@ -293,6 +300,7 @@ function plan_state_from_file(path) {
|
||||
byedpi_runtime: as_string(state.byedpi_runtime_signature),
|
||||
wdtt_runtime: as_string(state.wdtt_runtime_signature),
|
||||
olcrtc_runtime: as_string(state.olcrtc_runtime_signature),
|
||||
fptn_runtime: as_string(state.fptn_runtime_signature),
|
||||
list: as_string(state.list_signature),
|
||||
hosts_list: as_string(state.hosts_list_signature),
|
||||
cron: as_string(state.cron_signature),
|
||||
|
||||
@@ -132,6 +132,7 @@ const RELOAD_STATE_FIELDS = [
|
||||
"byedpi_runtime_signature",
|
||||
"wdtt_runtime_signature",
|
||||
"olcrtc_runtime_signature",
|
||||
"fptn_runtime_signature",
|
||||
"list_signature",
|
||||
"hosts_list_signature",
|
||||
"cron_signature",
|
||||
@@ -1854,6 +1855,24 @@ function olcrtc_runtime_signature_body(sections) {
|
||||
return body;
|
||||
}
|
||||
|
||||
function fptn_runtime_signature_body(sections) {
|
||||
let body = "";
|
||||
|
||||
for (let section in sections) {
|
||||
section = object_or_empty(section);
|
||||
if (!section_action_is_enabled(section, "fptn"))
|
||||
continue;
|
||||
|
||||
let name = section_name(section);
|
||||
body = signature_add_value(body, "fptn." + name + ".access_token", option(section, "access_token", ""));
|
||||
body = signature_add_value(body, "fptn." + name + ".sni", option(section, "sni", ""));
|
||||
body = signature_add_value(body, "fptn." + name + ".bypass_method", option(section, "bypass_method", ""));
|
||||
body = signature_add_value(body, "fptn." + name + ".preferred_server", option(section, "preferred_server", ""));
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
function reload_state_values_from_sources(format, settings, sections, servers, dnsmasq, legacy_dnsmasq_present, mwan3_active_value, schedules, profiles, guest_modes) {
|
||||
return {
|
||||
format: as_string(format),
|
||||
@@ -1868,6 +1887,7 @@ function reload_state_values_from_sources(format, settings, sections, servers, d
|
||||
byedpi_runtime_signature: signature_hash(byedpi_runtime_signature_body(sections)),
|
||||
wdtt_runtime_signature: signature_hash(wdtt_runtime_signature_body(sections)),
|
||||
olcrtc_runtime_signature: signature_hash(olcrtc_runtime_signature_body(sections)),
|
||||
fptn_runtime_signature: signature_hash(fptn_runtime_signature_body(sections)),
|
||||
list_signature: signature_hash(list_update_signature_body(sections)),
|
||||
hosts_list_signature: signature_hash(hosts_list_signature_body(sections)),
|
||||
cron_signature: signature_hash(cron_signature_body(settings, sections)),
|
||||
@@ -2276,6 +2296,12 @@ else if (mode == "olcrtc-runtime-signature-fixture") {
|
||||
let data = fixture_data(ARGV[1]);
|
||||
exit(print_signature_hash(olcrtc_runtime_signature_body(fixture_section_list(data))) ? 0 : 1);
|
||||
}
|
||||
else if (mode == "fptn-runtime-signature")
|
||||
exit(print_signature_hash(fptn_runtime_signature_body(uci_sections("section"))) ? 0 : 1);
|
||||
else if (mode == "fptn-runtime-signature-fixture") {
|
||||
let data = fixture_data(ARGV[1]);
|
||||
exit(print_signature_hash(fptn_runtime_signature_body(fixture_section_list(data))) ? 0 : 1);
|
||||
}
|
||||
else if (mode == "dont-touch-dhcp")
|
||||
print(dont_touch_dhcp_value(uci_settings()), "\n");
|
||||
else if (mode == "dont-touch-dhcp-fixture") {
|
||||
|
||||
Reference in New Issue
Block a user