feat(fptn): integrate FPTN client component and runtime provider

This commit is contained in:
Dushnilin
2026-09-15 14:57:05 +03:00
parent 03c51d7606
commit 73cdde17bb
26 changed files with 880 additions and 14 deletions
+116
View File
@@ -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")
+47 -1
View File
@@ -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")
+4 -2
View File
@@ -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;
}
+17 -1
View File
@@ -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
};
+11
View File
@@ -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
+8
View File
@@ -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),
+26
View File
@@ -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") {