fix(fptn): boot recovery, reload planner, persistent selector state and telegram token flow
This commit is contained in:
@@ -756,13 +756,13 @@ function rule_action_supported(action) {
|
||||
return contains([ "connection", "proxy", "outbound", "vpn", "awg", "warp",
|
||||
"anytls", "snell", "mieru", "sudoku", "masque", "openvpn",
|
||||
"bypass", "block", "dns", "zapret", "zapret2", "byedpi", "hosts",
|
||||
"wdtt", "olcrtc" ], as_string(action));
|
||||
"wdtt", "olcrtc", "fptn" ], as_string(action));
|
||||
}
|
||||
|
||||
function server_routing_section_action_supported(action) {
|
||||
return contains([ "connection", "proxy", "outbound", "vpn", "awg", "warp",
|
||||
"anytls", "snell", "mieru", "sudoku", "masque", "openvpn",
|
||||
"zapret", "zapret2", "byedpi", "wdtt", "olcrtc" ], as_string(action));
|
||||
"zapret", "zapret2", "byedpi", "wdtt", "olcrtc", "fptn" ], as_string(action));
|
||||
}
|
||||
|
||||
function duration_to_seconds_value(value) {
|
||||
@@ -1517,6 +1517,13 @@ function validate_rule(section, sections, context) {
|
||||
validate_olcrtc_section(section, context);
|
||||
}
|
||||
|
||||
if (action == "fptn") {
|
||||
let fptn_validator = require("providers.fptn.validator");
|
||||
let vres = fptn_validator.validate_section(section);
|
||||
if (!vres.valid)
|
||||
fail_validation("FPTN rule '" + name + "': " + vres.error + ". Aborted.");
|
||||
}
|
||||
|
||||
if (connections.is_connections_action(action)) {
|
||||
validate_dashboard_filter(section);
|
||||
|
||||
@@ -2058,6 +2065,9 @@ function context_from_runtime() {
|
||||
zapret2_installed: file_executable(constant_value(constants, "ZAPRET2_PROVIDER_NFQWS2_BIN")),
|
||||
wdtt_installed: file_executable(constant_value(constants, "WDTT_BIN")),
|
||||
olcrtc_installed: file_executable(constant_value(constants, "OLCRTC_BIN")),
|
||||
fptn_installed: file_executable("/usr/bin/fptn-client-cli") || file_executable("/usr/bin/fptn-client"),
|
||||
fptn_bin: file_executable("/usr/bin/fptn-client-cli") ? "/usr/bin/fptn-client-cli" : "/usr/bin/fptn-client",
|
||||
fptn_state_dir: "/var/run/tachyon/fptn",
|
||||
zapret_provider_nfqws_bin: constant_value(constants, "ZAPRET_PROVIDER_NFQWS_BIN"),
|
||||
zapret2_provider_nfqws2_bin: constant_value(constants, "ZAPRET2_PROVIDER_NFQWS2_BIN"),
|
||||
zapret_route_mark_base: constant_value(constants, "ZAPRET_ROUTE_MARK_BASE"),
|
||||
@@ -2367,7 +2377,7 @@ function has_outbound_section(ctx) {
|
||||
// without this check a lone AWG/WARP/VPN section triggered the
|
||||
// misleading "No proxy outbound sections found" warning.
|
||||
if (contains([ "awg", "warp", "vpn", "openvpn", "masque",
|
||||
"anytls", "snell", "mieru", "sudoku" ], as_string(action)))
|
||||
"anytls", "snell", "mieru", "sudoku", "fptn" ], as_string(action)))
|
||||
return true;
|
||||
|
||||
if (length(connections.connection_urls(section)) > 0 ||
|
||||
@@ -2451,6 +2461,15 @@ function check_provider_requirements(ctx) {
|
||||
"ByeDPI provider is not available at " + ctx.byedpi_bin + ". Rules with action 'byedpi' will be skipped until the byedpi package is installed.",
|
||||
"Failed to prepare the Tachyon ByeDPI state directory in " + ctx.byedpi_state_dir + ". Aborted."
|
||||
);
|
||||
|
||||
check_provider_requirement(
|
||||
"fptn",
|
||||
"FPTN",
|
||||
ctx.fptn_bin,
|
||||
[ ctx.fptn_state_dir ],
|
||||
"FPTN provider is not available at " + ctx.fptn_bin + ". Rules with action 'fptn' will be skipped until the fptn package is installed.",
|
||||
"Failed to prepare the Tachyon FPTN state directory in " + ctx.fptn_state_dir + ". Aborted."
|
||||
);
|
||||
}
|
||||
|
||||
function check_runtime_requirements() {
|
||||
|
||||
@@ -1825,6 +1825,19 @@ function latency_test_url() {
|
||||
return value == "" ? DEFAULT_LATENCY_TEST_URL : value;
|
||||
}
|
||||
|
||||
function save_persistent_selector_choice(group_tag, proxy_tag) {
|
||||
group_tag = as_string(group_tag);
|
||||
proxy_tag = as_string(proxy_tag);
|
||||
if (group_tag == "" || proxy_tag == "")
|
||||
return false;
|
||||
let path = getenv("TACHYON_PERSISTENT_SELECTOR_STATE_FILE") || "/etc/tachyon/selector_state.json";
|
||||
let state = common.read_json_file(path);
|
||||
if (type(state) != "object")
|
||||
state = {};
|
||||
state[group_tag] = proxy_tag;
|
||||
return common.write_json_file(path, state, 2);
|
||||
}
|
||||
|
||||
function clash_api(action, arg1, arg2, arg3) {
|
||||
let base_url = clash_api_url();
|
||||
let test_url = latency_test_url();
|
||||
@@ -1997,13 +2010,31 @@ function clash_api(action, arg1, arg2, arg3) {
|
||||
push(args, "--data-raw");
|
||||
push(args, payload);
|
||||
let result = status_capture([ "clash-set-group-proxy-result", arg1, arg2 ], command_output(command_from_args(args)));
|
||||
if (result.status == 0)
|
||||
if (result.status == 0) {
|
||||
command_status("conntrack -F >/dev/null 2>&1 || true");
|
||||
save_persistent_selector_choice(arg1, arg2);
|
||||
}
|
||||
if (result.output != "")
|
||||
print(result.output);
|
||||
return result.status;
|
||||
}
|
||||
|
||||
if (action == "restore_selector_state") {
|
||||
let path = getenv("TACHYON_PERSISTENT_SELECTOR_STATE_FILE") || "/etc/tachyon/selector_state.json";
|
||||
let state = common.read_json_file(path);
|
||||
if (type(state) != "object" || length(keys(state)) == 0)
|
||||
return 0;
|
||||
let proxies_res = clash_json_data([ "curl", "-s", base_url + "/proxies" ], auth);
|
||||
let proxies = object_or_empty(object_or_empty(proxies_res).proxies);
|
||||
for (let group, selected in state) {
|
||||
let grp = proxies[group];
|
||||
if (type(grp) == "object" && lc(as_string(grp.type || "")) == "selector" && index(grp.all, selected) >= 0 && as_string(grp.now || "") != selected) {
|
||||
clash_api("set_group_proxy", group, selected);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (action == "close_connection") {
|
||||
if (as_string(arg1) == "")
|
||||
return clash_json_error("connection_id required");
|
||||
|
||||
@@ -145,26 +145,10 @@ function stop_runtime() {
|
||||
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];
|
||||
function launch_fptn_process(section) {
|
||||
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");
|
||||
if (token == "")
|
||||
return false;
|
||||
}
|
||||
|
||||
stop_runtime();
|
||||
|
||||
command_status("mkdir -p " + shell_quote(cfg.state_dir) + " 2>/dev/null");
|
||||
|
||||
@@ -196,6 +180,95 @@ function start_runtime() {
|
||||
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++) {
|
||||
@@ -210,11 +283,24 @@ function start_runtime() {
|
||||
}
|
||||
|
||||
if (!started) {
|
||||
log_message("FPTN interface " + cfg.tun_interface + " did not come up", "warn");
|
||||
return false;
|
||||
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;
|
||||
}
|
||||
|
||||
install_kernel_routing();
|
||||
log_message("FPTN client successfully started on " + cfg.tun_interface + " (table " + cfg.route_table + ")", "info");
|
||||
return true;
|
||||
}
|
||||
@@ -225,20 +311,42 @@ function status_json() {
|
||||
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: installed && running && rule_count > 0,
|
||||
status_message: running ? "FPTN is running" : (installed ? "FPTN is installed but not running" : "FPTN is not installed")
|
||||
ready: ready,
|
||||
status_message: status_msg
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -258,6 +366,8 @@ function module_exports() {
|
||||
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
|
||||
};
|
||||
@@ -275,6 +385,10 @@ 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")
|
||||
@@ -286,6 +400,6 @@ else if (mode == "package-version" || mode == "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");
|
||||
warn("Usage: providers/fptn/runtime.uc <start-runtime|stop-runtime|restart-runtime|ensure-routing|supervise-runtime|status|check|installed|version>\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
@@ -556,6 +556,16 @@ let strings = {
|
||||
critical_keyword_fallen: "Down",
|
||||
critical_keyword_error: "Error",
|
||||
btn_update_component: "Update",
|
||||
fptn_token_title: "FPTN Access Token",
|
||||
fptn_token_prompt: "Send the new FPTN access token (copy/forward from @fptn_bot):",
|
||||
fptn_token_invalid: "Invalid or empty FPTN token. Please provide a valid token.",
|
||||
fptn_token_no_section: "Token saved, but no section with action 'fptn' was found.",
|
||||
fptn_restarting: "Updating token and connecting FPTN...",
|
||||
fptn_token_updated_title: "FPTN Token Updated and Connected!",
|
||||
fptn_token_saved_connecting: "Token saved. Connecting to FPTN...",
|
||||
fptn_token_retry_hint: "Connecting in background; routing will be applied automatically.",
|
||||
fptn_token_change_btn: "FPTN Token",
|
||||
fptn_token_btn: "FPTN Token",
|
||||
},
|
||||
|
||||
ru: {
|
||||
@@ -1113,6 +1123,16 @@ let strings = {
|
||||
critical_keyword_fallen: "Упал",
|
||||
critical_keyword_error: "Ошибка",
|
||||
btn_update_component: "Обновить",
|
||||
fptn_token_title: "FPTN Access Token",
|
||||
fptn_token_prompt: "Пришлите новый токен доступа FPTN (скопируйте или перешлите из @fptn_bot):",
|
||||
fptn_token_invalid: "Некорректный или пустой токен FPTN. Попробуйте ещё раз.",
|
||||
fptn_token_no_section: "Токен сохранён, но секция с действием 'fptn' не найдена в конфиге.",
|
||||
fptn_restarting: "Обновляю токен и запускаю FPTN...",
|
||||
fptn_token_updated_title: "FPTN токен успешно обновлён и подключён!",
|
||||
fptn_token_saved_connecting: "Токен сохранён, но FPTN пока подключается.",
|
||||
fptn_token_retry_hint: "Подключение продолжится в фоне; маршрутизация применится автоматически.",
|
||||
fptn_token_change_btn: "Сменить токен FPTN",
|
||||
fptn_token_btn: "Токен FPTN",
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -435,10 +435,26 @@ function clash_api_json(action, arg1, arg2, arg3) {
|
||||
}
|
||||
|
||||
function capture_selector_state() {
|
||||
return selector_state_from_proxies_payload(clash_api_json("get_proxies"));
|
||||
let state = selector_state_from_proxies_payload(clash_api_json("get_proxies"));
|
||||
if (type(state) == "object" && length(keys(state)) > 0) {
|
||||
let persistent_path = getenv("TACHYON_PERSISTENT_SELECTOR_STATE_FILE") || "/etc/tachyon/selector_state.json";
|
||||
let disk_state = common.read_json_file(persistent_path);
|
||||
if (type(disk_state) != "object")
|
||||
disk_state = {};
|
||||
for (let k, v in state)
|
||||
disk_state[k] = v;
|
||||
common.write_json_file(persistent_path, disk_state, 2);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function restore_selector_state(snapshot) {
|
||||
if (snapshot == null || (type(snapshot) == "object" && length(keys(snapshot)) == 0)) {
|
||||
let persistent_path = getenv("TACHYON_PERSISTENT_SELECTOR_STATE_FILE") || "/etc/tachyon/selector_state.json";
|
||||
let disk_state = common.read_json_file(persistent_path);
|
||||
if (type(disk_state) == "object" && length(keys(disk_state)) > 0)
|
||||
snapshot = disk_state;
|
||||
}
|
||||
let pairs = selector_restore_pairs(snapshot, clash_api_json("get_proxies"));
|
||||
|
||||
for (let pair in pairs)
|
||||
@@ -948,6 +964,8 @@ function start_main() {
|
||||
module_success(PARENTAL_QUOTA_UC, [ "install-cron" ]);
|
||||
module_success(PARENTAL_QUOTA_UC, [ "tick" ]);
|
||||
|
||||
restore_selector_state(null);
|
||||
|
||||
module_background(UI_UC, [ "latency-boot-sweep" ]);
|
||||
module_background(UPDATES_UC, [ "list-update" ]);
|
||||
module_background(LIB_DIR + "/service/lifecycle.uc", [ "discover-awg-mtu" ]);
|
||||
|
||||
@@ -159,6 +159,9 @@ function emit_reload_plan(previous, current, context) {
|
||||
!needs.zapret_restart &&
|
||||
!needs.zapret2_restart &&
|
||||
!needs.byedpi_restart &&
|
||||
!needs.wdtt_restart &&
|
||||
!needs.olcrtc_restart &&
|
||||
!needs.fptn_restart &&
|
||||
!needs.dnsmasq_configure &&
|
||||
!needs.dnsmasq_restore)
|
||||
needs.sing_box_reload = true;
|
||||
@@ -169,6 +172,9 @@ function emit_reload_plan(previous, current, context) {
|
||||
needs.zapret_restart ||
|
||||
needs.zapret2_restart ||
|
||||
needs.byedpi_restart ||
|
||||
needs.wdtt_restart ||
|
||||
needs.olcrtc_restart ||
|
||||
needs.fptn_restart ||
|
||||
needs.dnsmasq_configure ||
|
||||
needs.dnsmasq_restore ||
|
||||
needs.cron_refresh ||
|
||||
|
||||
@@ -882,6 +882,20 @@ function view_menu(token, chat_id, msg_id) {
|
||||
{ text: t("menu_help"), callback_data: "/help" }
|
||||
]
|
||||
];
|
||||
|
||||
let has_fptn = false;
|
||||
let sections = api.get_sections();
|
||||
for (let i = 0; i < length(sections); i++) {
|
||||
if (sections[i].action == "fptn") {
|
||||
has_fptn = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (has_fptn) {
|
||||
push(keyboard, [
|
||||
{ text: "🔑 " + t("fptn_token_btn"), callback_data: "/fptn_token" }
|
||||
]);
|
||||
}
|
||||
|
||||
if (msg_id) edit_message(token, chat_id, msg_id, text, "HTML", keyboard);
|
||||
else send_message(token, chat_id, text, "HTML", keyboard);
|
||||
@@ -1046,6 +1060,130 @@ function view_outbounds(token, chat_id, msg_id, group_name) {
|
||||
else send_message(token, chat_id, text, "HTML", keyboard);
|
||||
}
|
||||
|
||||
function save_persistent_selector_choice(group_tag, proxy_tag) {
|
||||
group_tag = as_string(group_tag);
|
||||
proxy_tag = as_string(proxy_tag);
|
||||
if (group_tag == "" || proxy_tag == "")
|
||||
return false;
|
||||
let path = getenv("TACHYON_PERSISTENT_SELECTOR_STATE_FILE") || "/etc/tachyon/selector_state.json";
|
||||
let state = common.read_json_file(path);
|
||||
if (type(state) != "object")
|
||||
state = {};
|
||||
state[group_tag] = proxy_tag;
|
||||
return common.write_json_file(path, state, 2);
|
||||
}
|
||||
|
||||
function extract_clean_fptn_token(raw) {
|
||||
let text = trim(as_string(raw));
|
||||
if (text == "") return "";
|
||||
let m = match(text, /(?:access_token|token)[\s:=]+["']?([A-Za-z0-9_\-\.]{8,})["']?/i);
|
||||
if (m && m[1])
|
||||
return trim(m[1]);
|
||||
m = match(text, /^["']([A-Za-z0-9_\-\.]{8,})["']$/);
|
||||
if (m && m[1])
|
||||
return trim(m[1]);
|
||||
m = match(text, /^([A-Za-z0-9_\-\.]{8,})$/);
|
||||
if (m && m[1])
|
||||
return trim(m[1]);
|
||||
return "";
|
||||
}
|
||||
|
||||
function mask_fptn_token(tok) {
|
||||
tok = as_string(tok);
|
||||
if (length(tok) > 8)
|
||||
return "••••••••" + substr(tok, length(tok) - 4);
|
||||
if (length(tok) > 0)
|
||||
return "••••";
|
||||
return t("status_disabled");
|
||||
}
|
||||
|
||||
function handle_fptn_token_update(token, chat_id, raw_input, target_sec) {
|
||||
let clean_tok = extract_clean_fptn_token(raw_input);
|
||||
if (clean_tok == "") {
|
||||
send_message(token, chat_id, "❌ " + t("fptn_token_invalid"), "HTML", [
|
||||
[{ text: "⬅️ " + t("nav_menu"), callback_data: "/menu" }]
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
let c = uci_core.cursor();
|
||||
c.load(CONFIG_NAME);
|
||||
let all = c.get_all(CONFIG_NAME);
|
||||
let fptn_count = 0;
|
||||
|
||||
if (target_sec && all[target_sec]) {
|
||||
c.set(CONFIG_NAME, target_sec, "access_token", clean_tok);
|
||||
fptn_count++;
|
||||
} else {
|
||||
for (let sname in keys(all)) {
|
||||
let s = all[sname];
|
||||
if (type(s) == "object" && s.action == "fptn") {
|
||||
c.set(CONFIG_NAME, sname, "access_token", clean_tok);
|
||||
fptn_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.commit(CONFIG_NAME);
|
||||
|
||||
try {
|
||||
let fc = uci_core.cursor();
|
||||
if (fc.load("fptn")) {
|
||||
if (fc.get_all("fptn", "config")) {
|
||||
fc.set("fptn", "config", "access_token", clean_tok);
|
||||
fc.commit("fptn");
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
let masked = mask_fptn_token(clean_tok);
|
||||
|
||||
if (fptn_count == 0) {
|
||||
let msg = "⚠️ <b>" + t("fptn_token_no_section") + "</b>\nToken: <code>" + escape_html(masked) + "</code>";
|
||||
send_message(token, chat_id, msg, "HTML", [
|
||||
[{ text: "⬅️ " + t("nav_menu"), callback_data: "/menu" }]
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
|
||||
send_message(token, chat_id, "⏳ <b>" + t("fptn_token_saved_connecting") + "</b>\nToken: <code>" + escape_html(masked) + "</code>", "HTML");
|
||||
|
||||
// Restart FPTN runtime
|
||||
command_status(command_from_args([ "ucode", "-L", LIB_DIR, LIB_DIR + "/providers/fptn/runtime.uc", "restart-runtime" ]));
|
||||
|
||||
// Poll up to 5 seconds for tun-fptn and table 4249 routing
|
||||
let connected = false;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
let out = command_output_from_args([ "ucode", "-L", LIB_DIR, LIB_DIR + "/providers/fptn/runtime.uc", "status" ]);
|
||||
try {
|
||||
let st = json(out);
|
||||
if (st && st.running && st.tun_up && st.route_installed) {
|
||||
connected = true;
|
||||
break;
|
||||
}
|
||||
} catch (e) {}
|
||||
command_status(command_from_args([ "sleep", "1" ]));
|
||||
}
|
||||
|
||||
if (connected) {
|
||||
let msg = "✅ <b>" + t("fptn_token_updated_title") + "</b>\n" +
|
||||
"Token: <code>" + escape_html(masked) + "</code>\n" +
|
||||
"Status: 🟢 Connected (tun-fptn & routing ready)";
|
||||
send_message(token, chat_id, msg, "HTML", [
|
||||
[{ text: "⬅️ " + t("nav_menu"), callback_data: "/menu" }]
|
||||
]);
|
||||
} else {
|
||||
let msg = "⚠️ <b>" + t("fptn_token_saved_connecting") + "</b>\n" +
|
||||
"Token: <code>" + escape_html(masked) + "</code>\n\n" +
|
||||
t("fptn_token_retry_hint");
|
||||
send_message(token, chat_id, msg, "HTML", [
|
||||
[{ text: "⬅️ " + t("nav_menu"), callback_data: "/menu" }]
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function handle_switch(token, chat_id, msg_id, group_name, server_name) {
|
||||
api.clash_request("PUT", "proxies/" + group_name, { name: server_name });
|
||||
// A successful switch answers 204 with an empty body, which clash_request()
|
||||
@@ -1058,6 +1196,8 @@ function handle_switch(token, chat_id, msg_id, group_name, server_name) {
|
||||
t("outbound_group") + ": <code>" + escape_html(group_name) + "</code>\n" +
|
||||
t("outbound_server") + ": <code>" + escape_html(server_name) + "</code>\n\n" +
|
||||
t("outbound_check_hint"), "HTML");
|
||||
} else {
|
||||
save_persistent_selector_choice(group_name, server_name);
|
||||
}
|
||||
view_outbounds(token, chat_id, msg_id, group_name);
|
||||
}
|
||||
@@ -1094,6 +1234,10 @@ function view_section_editor(token, chat_id, msg_id, sec_name) {
|
||||
if (s.action == "proxy" || s.action == "route") {
|
||||
text += t("status_target") + ": <code>" + escape_html(s.target || "main-out") + "</code>\n";
|
||||
}
|
||||
if (s.action == "fptn") {
|
||||
let tok = s.access_token || "";
|
||||
text += "🔑 Token: <code>" + escape_html(mask_fptn_token(tok)) + "</code>\n";
|
||||
}
|
||||
|
||||
let d_count = length(common.list_option(s, "domain")) + length(common.list_option(s, "domain_suffix")) + length(common.list_option(s, "domain_keyword")) + length(common.list_option(s, "domain_regex"));
|
||||
let ip_count = length(common.list_option(s, "ip")) + length(common.list_option(s, "ip_cidr"));
|
||||
@@ -1116,6 +1260,9 @@ function view_section_editor(token, chat_id, msg_id, sec_name) {
|
||||
if (s.action == "proxy" || s.action == "route") {
|
||||
push(keyboard, [{ text: t("status_target") + ": " + (s.target || "main-out"), callback_data: "/sec_target " + sec_name }]);
|
||||
}
|
||||
if (s.action == "fptn") {
|
||||
push(keyboard, [{ text: "🔑 " + t("fptn_token_change_btn"), callback_data: "/fptn_token " + sec_name }]);
|
||||
}
|
||||
|
||||
push(keyboard, [
|
||||
{ text: t("section_domains") + " (" + d_count + ")", callback_data: "/sec_list " + sec_name + " domain" },
|
||||
@@ -2026,7 +2173,8 @@ function register_bot_commands(token) {
|
||||
{ command: "close_connections", description: t("cmd_close_connections") },
|
||||
{ command: "doctor", description: t("cmd_doctor") },
|
||||
{ command: "restart", description: t("cmd_restart") },
|
||||
{ command: "lang", description: t("cmd_lang") }
|
||||
{ command: "lang", description: t("cmd_lang") },
|
||||
{ command: "fptn", description: t("fptn_token_btn") }
|
||||
];
|
||||
tg_request(token, "setMyCommands", { commands: commands });
|
||||
}
|
||||
@@ -2537,6 +2685,32 @@ function dispatch_command(token, chat_id, text, msg_id) {
|
||||
let grp = trim(join(" ", slice(parts, 1)));
|
||||
return view_outbounds(token, chat_id, msg_id, grp);
|
||||
}
|
||||
|
||||
if (cmd == "/fptn_token" || cmd == "/fptn") {
|
||||
set_tg_state(chat_id, { action: "fptn_token" });
|
||||
return send_message(token, chat_id,
|
||||
"🔑 <b>" + t("fptn_token_title") + "</b>\n\n" +
|
||||
t("fptn_token_prompt") + "\n\n<i>Отправьте /cancel для отмены</i>", "HTML");
|
||||
}
|
||||
if (match(cmd, /^\/(fptn_token|fptn)[ \t]+/)) {
|
||||
let raw_token = trim(replace(cmd, /^\/(fptn_token|fptn)[ \t]+/, ""));
|
||||
let all_sections = api.get_sections();
|
||||
let target_sec = null;
|
||||
for (let i = 0; i < length(all_sections); i++) {
|
||||
let s = all_sections[i];
|
||||
if (s[".name"] == raw_token || s.name == raw_token) {
|
||||
target_sec = raw_token;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (target_sec) {
|
||||
set_tg_state(chat_id, { action: "fptn_token", sec: target_sec });
|
||||
return send_message(token, chat_id,
|
||||
"🔑 <b>" + t("fptn_token_title") + " (" + escape_html(target_sec) + ")</b>\n\n" +
|
||||
t("fptn_token_prompt") + "\n\n<i>Отправьте /cancel для отмены</i>", "HTML");
|
||||
}
|
||||
return handle_fptn_token_update(token, chat_id, raw_token, null);
|
||||
}
|
||||
|
||||
if (cmd == "/sections" || cmd == "/rules") return view_sections(token, chat_id, msg_id);
|
||||
if (cmd == "/devices") return view_devices(token, chat_id, msg_id);
|
||||
@@ -3053,6 +3227,10 @@ function process_updates(token, admin_ids) {
|
||||
view_quiet_hours(token, chat_id, null);
|
||||
}
|
||||
}
|
||||
else if (state.action == "fptn_token") {
|
||||
set_tg_state(chat_id, null);
|
||||
handle_fptn_token_update(token, chat_id, trim(msg.text), state.sec || null);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3538,11 +3716,14 @@ else if (mode == "send") {
|
||||
let msg_parts = [];
|
||||
for (let i = 2; i < length(ARGV); i++)
|
||||
push(msg_parts, ARGV[i]);
|
||||
let message = join(" ", msg_parts);
|
||||
if (message == "") { warn("No message text\n"); exit(1); }
|
||||
exit(send_api(message));
|
||||
}
|
||||
else if (mode == "mask-token") {
|
||||
print(mask_fptn_token(ARGV[1]), "\n");
|
||||
exit(0);
|
||||
}
|
||||
else {
|
||||
warn("Usage: service/telegram.uc <start-runtime|stop-runtime|worker|status|diagnose|send ...> ...\n");
|
||||
warn("Usage: service/telegram.uc <start-runtime|stop-runtime|worker|status|diagnose|send|mask-token ...> ...\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ let bool_option = common.bool_option;
|
||||
let int_option = common.int_option;
|
||||
let array_or_empty = common.array_or_empty;
|
||||
let object_or_empty = common.object_or_empty;
|
||||
let read_json_file = common.read_json_file;
|
||||
|
||||
let outbound_tag = runtime_constants.outbound_tag;
|
||||
let tag = runtime_constants.tag;
|
||||
@@ -883,6 +884,14 @@ function add_proxy_selector(config, section, selector_tags, urltest_candidate_ta
|
||||
}
|
||||
}
|
||||
|
||||
let persistent_selector_file = getenv("TACHYON_PERSISTENT_SELECTOR_STATE_FILE") || "/etc/tachyon/selector_state.json";
|
||||
let saved_selector_state = read_json_file(persistent_selector_file);
|
||||
if (type(saved_selector_state) == "object" && as_string(saved_selector_state[selector_tag]) != "") {
|
||||
let saved_target = as_string(saved_selector_state[selector_tag]);
|
||||
if (index(selector_outbounds, saved_target) >= 0)
|
||||
selector_default = saved_target;
|
||||
}
|
||||
|
||||
push(config.outbounds, {
|
||||
type: "selector",
|
||||
tag: selector_tag,
|
||||
|
||||
Reference in New Issue
Block a user