From 11925e7ec5ea4cce277a6d1e30798753e78834be Mon Sep 17 00:00:00 2001 From: Dushnilin Date: Sat, 5 Sep 2026 23:52:16 +0300 Subject: [PATCH] fix(updates): resolve tag commit SHA and format build fingerprints; fix telegram, watchdog and logs --- .github/workflows/build.yml | 3 + fe-app-tachyon/locales/tachyon.ru.po | 3 + .../partials/renderUpdateProgressModal.ts | 4 +- install.sh | 2 +- .../resources/view/tachyon/main.js | 4 +- luci-app-tachyon/po/ru/tachyon.po | 3 + tachyon/files/usr/lib/components/action.uc | 70 ++++++++++- tachyon/files/usr/lib/components/updater.uc | 12 +- tachyon/files/usr/lib/core/common.uc | 37 ++++-- tachyon/files/usr/lib/diagnostics/runtime.uc | 12 +- tachyon/files/usr/lib/diagnostics/status.uc | 12 +- .../files/usr/lib/service/event_controller.uc | 56 ++++++++- tachyon/files/usr/lib/service/telegram.uc | 111 +++++++++++------- tachyon/files/usr/lib/service/watchdog.uc | 20 ++-- tachyon/files/usr/lib/singbox/runtime.uc | 2 +- 15 files changed, 260 insertions(+), 91 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0f56ee44..195c833c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -94,11 +94,14 @@ jobs: files: ./filtered-bin/release/* tag_name: ${{ github.ref_name }} name: ${{ github.ref_name }} + target_commitish: ${{ needs.preparation.outputs.commitish }} generate_release_notes: true draft: false prerelease: false make_latest: true overwrite_files: true + body: | + Commit: ${{ needs.preparation.outputs.commitish }} - name: Release on workflow dispatch if: ${{ github.event_name == 'workflow_dispatch' }} diff --git a/fe-app-tachyon/locales/tachyon.ru.po b/fe-app-tachyon/locales/tachyon.ru.po index 814f2f1b..2018c4ca 100644 --- a/fe-app-tachyon/locales/tachyon.ru.po +++ b/fe-app-tachyon/locales/tachyon.ru.po @@ -5932,3 +5932,6 @@ msgstr "Конфигурация AmneziaWG %s успешно сгенериро msgid "Notice: Cloudflare WARP edge servers only support AmneziaWG 2.0 (standard headers). For Cloudflare WARP, select version 2.0. Versions 3.0 and 3.1 are intended for dedicated AmneziaWG servers." msgstr "Обратите внимание: серверы Cloudflare WARP поддерживают только AmneziaWG 2.0 (стандартные заголовки). Для Cloudflare WARP выберите версию 2.0. Версии 3.0 и 3.1 предназначены для выделенных серверов AmneziaWG." + +msgid "new build" +msgstr "новая сборка" diff --git a/fe-app-tachyon/src/tachyon/tabs/updates/partials/renderUpdateProgressModal.ts b/fe-app-tachyon/src/tachyon/tabs/updates/partials/renderUpdateProgressModal.ts index df6620ce..1c21418c 100644 --- a/fe-app-tachyon/src/tachyon/tabs/updates/partials/renderUpdateProgressModal.ts +++ b/fe-app-tachyon/src/tachyon/tabs/updates/partials/renderUpdateProgressModal.ts @@ -70,9 +70,9 @@ function renderVersionBadgeText(opts: { return `${cVer} (${tSha})`; } if (cSha) { - return `${cVer} (${cSha})`; + return `${cVer} (${cSha} → ${_('new build')})`; } - return `${cVer} → ${tVer}`; + return `${cVer} (${_('new build')})`; } const currentPart = cVer ? (cSha ? `${cVer} (${cSha})` : cVer) : ''; diff --git a/install.sh b/install.sh index b7d390c8..d2de852b 100755 --- a/install.sh +++ b/install.sh @@ -2418,7 +2418,7 @@ install_selected_sing_box() { fi [ -x /usr/bin/tachyon ] || fail "tachyon backend must be installed before sing-box component action" - for kmod in inet_diag netlink_diag tun nft_tproxy; do + for kmod in inet_diag netlink_diag tun nft_tproxy nft_nat; do modprobe "$kmod" 2>/dev/null || true done [ -d /dev/net ] || mkdir -p /dev/net diff --git a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js index 9ec97810..db022b84 100644 --- a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js +++ b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js @@ -19041,9 +19041,9 @@ function renderVersionBadgeText(opts) { return `${cVer} (${tSha})`; } if (cSha) { - return `${cVer} (${cSha})`; + return `${cVer} (${cSha} \u2192 ${_("new build")})`; } - return `${cVer} \u2192 ${tVer}`; + return `${cVer} (${_("new build")})`; } const currentPart = cVer ? cSha ? `${cVer} (${cSha})` : cVer : ""; const targetPart = tSha ? `${tVer} (${tSha})` : tVer; diff --git a/luci-app-tachyon/po/ru/tachyon.po b/luci-app-tachyon/po/ru/tachyon.po index 814f2f1b..2018c4ca 100644 --- a/luci-app-tachyon/po/ru/tachyon.po +++ b/luci-app-tachyon/po/ru/tachyon.po @@ -5932,3 +5932,6 @@ msgstr "Конфигурация AmneziaWG %s успешно сгенериро msgid "Notice: Cloudflare WARP edge servers only support AmneziaWG 2.0 (standard headers). For Cloudflare WARP, select version 2.0. Versions 3.0 and 3.1 are intended for dedicated AmneziaWG servers." msgstr "Обратите внимание: серверы Cloudflare WARP поддерживают только AmneziaWG 2.0 (стандартные заголовки). Для Cloudflare WARP выберите версию 2.0. Версии 3.0 и 3.1 предназначены для выделенных серверов AmneziaWG." + +msgid "new build" +msgstr "новая сборка" diff --git a/tachyon/files/usr/lib/components/action.uc b/tachyon/files/usr/lib/components/action.uc index 8dbc61c9..1f37c53c 100644 --- a/tachyon/files/usr/lib/components/action.uc +++ b/tachyon/files/usr/lib/components/action.uc @@ -675,6 +675,54 @@ function fetch_github_release_by_tag_json(owner, repo, tag) { return response; } +function fetch_github_tag_commit_sha(owner, repo, tag) { + tag = trim(as_string(tag)); + if (tag == "") + return ""; + let url = "https://api.github.com/repos/" + as_string(owner) + "/" + as_string(repo) + "/commits/" + tag; + let response = http_get(url); + if (response == "" || !helper_success_input(response, "github-response-ok", [])) { + response = http_get("https://gh-proxy.com/" + url); + if (response == "" || !helper_success_input(response, "github-response-ok", [])) + return ""; + } + return trim(helper_output_input(response, "commit-object-sha", [])); +} + +function format_fingerprint_human(fp) { + fp = as_string(fp); + if (str_startswith(fp, "sha:")) + return substr(fp, 4, 7); + if (!str_startswith(fp, "build:")) + return fp != "" ? fp : "unknown"; + let body = substr(fp, 6); + let pairs = split(body, "|"); + let upd = ""; + let size = ""; + for (let p in pairs) { + if (str_startswith(p, "upd=")) + upd = substr(p, 4); + else if (str_startswith(p, "pub=") && upd == "") + upd = substr(p, 4); + else if (str_startswith(p, "size=")) + size = substr(p, 5); + } + let parts = []; + if (upd != "") { + let d = match(upd, /^([0-9]{4}-[0-9]{2}-[0-9]{2})T([0-9]{2}:[0-9]{2})/); + if (d && d[1] && d[2]) + push(parts, d[1] + " " + d[2] + " UTC"); + else + push(parts, upd); + } + if (size != "") { + let kb = int(int(size) / 1024); + if (kb > 0) + push(parts, kb + " KB"); + } + return length(parts) > 0 ? ("build (" + join(", ", parts) + ")") : fp; +} + function fetch_github_releases_json(owner, repo, per_page) { let url = "https://api.github.com/repos/" + as_string(owner) + "/" + as_string(repo) + "/releases?per_page=" + as_string(per_page || "10"); let response = http_get(url, "8"); @@ -875,6 +923,9 @@ function ensure_sing_box_dependencies() { command_success_from_args([ "modprobe", "tun" ]); command_success_from_args([ "modprobe", "inet_diag" ]); + command_success_from_args([ "modprobe", "netlink_diag" ]); + command_success_from_args([ "modprobe", "nft_tproxy" ]); + command_success_from_args([ "modprobe", "nft_nat" ]); if (!file_exists("/dev/net/tun")) { command_success_from_args([ "mkdir", "-p", "/dev/net" ]); command_success_from_args([ "mknod", "/dev/net/tun", "c", "10", "200" ]); @@ -1710,7 +1761,7 @@ function resolve_sing_box_lx_release(target_tag) { if (arch_suffix == "") return null; let base_dl = "https://github.com/Leadaxe/sing-box-lx/releases/download/" + target_tag + "/"; - let asset_name = "sing-box-lx_" + tag_clean + "_linux-" + arch_suffix + ".tar.gz"; + let asset_name = "sing-box-" + tag_clean + "-linux-" + arch_suffix + ".tar.gz"; return { tag: target_tag, release_url: "https://github.com/Leadaxe/sing-box-lx/releases/tag/" + target_tag, @@ -2481,8 +2532,17 @@ function check_tachyon() { remote_fingerprint = trim(helper_output_input(release_json, "release-build-fingerprint", [])); } + if (remote_sha == "" && latest_version != "unknown" && latest_version != "") { + let parts = split(TACHYON_RELEASE_REPO, "/"); + if (length(parts) == 2) { + remote_sha = fetch_github_tag_commit_sha(parts[0], parts[1], latest_version); + if (remote_sha != "" && (remote_fingerprint == "" || str_startswith(remote_fingerprint, "build:"))) + remote_fingerprint = "sha:" + remote_sha; + } + } + let sha_extra = null; - if (local_sha != "" || remote_fingerprint != "" || local_fingerprint != "") { + if (local_sha != "" || remote_sha != "" || remote_fingerprint != "" || local_fingerprint != "") { sha_extra = { current_sha: local_sha, latest_sha: remote_sha }; if (local_fingerprint != "") sha_extra.current_build = local_fingerprint; @@ -2495,9 +2555,11 @@ function check_tachyon() { // one, otherwise via the fingerprint recorded at install time. if (helper_success("tachyon-build-differs", [ local_sha, remote_sha, local_fingerprint, remote_fingerprint ])) { status = "outdated_same_release"; + let short_local = length(local_sha) >= 7 ? substr(local_sha, 0, 7) : local_sha; + let short_remote = length(remote_sha) >= 7 ? substr(remote_sha, 0, 7) : remote_sha; updates_log("Tachyon build update found for current release (" + TACHYON_VERSION + "): " + - (local_sha != "" && remote_sha != "" ? local_sha + " -> " + remote_sha : - local_fingerprint + " -> " + remote_fingerprint)); + (short_local != "" && short_remote != "" ? short_local + " -> " + short_remote : + format_fingerprint_human(local_fingerprint) + " -> " + format_fingerprint_human(remote_fingerprint))); action_success("tachyon", "check_update", "Update is available for current release", TACHYON_VERSION, latest_version, 0, status, release_url, sha_extra); } else { updates_log("Tachyon is already up to date (" + TACHYON_VERSION + ")"); diff --git a/tachyon/files/usr/lib/components/updater.uc b/tachyon/files/usr/lib/components/updater.uc index b5974da0..f55c18db 100644 --- a/tachyon/files/usr/lib/components/updater.uc +++ b/tachyon/files/usr/lib/components/updater.uc @@ -270,7 +270,8 @@ function release_commit_sha_value(release) { // ucode's regex engine rejects non-capturing groups, so the two spellings // are matched separately rather than as one alternation. for (let pattern in [ /[Cc]ommit:?[ \t]*([0-9a-fA-F]{7,40})/, - /[Ss][Hh][Aa]:?[ \t]*([0-9a-fA-F]{7,40})/ ]) { + /[Ss][Hh][Aa]:?[ \t]*([0-9a-fA-F]{7,40})/, + /\/commit\/([0-9a-fA-F]{7,40})/ ]) { let m = match(body, pattern); if (m && m[1]) return as_string(m[1]); @@ -280,6 +281,13 @@ function release_commit_sha_value(release) { return ""; } +function commit_object_sha() { + let obj = object_or_empty(read_stdin_json()); + let sha = as_string(obj.sha || ""); + if (sha != "" && match(sha, /^[0-9a-fA-F]{7,40}$/) != null) + print(sha, "\n"); +} + function release_commit_sha() { let sha = release_commit_sha_value(read_stdin_json()); if (sha != "") @@ -1406,6 +1414,8 @@ else if (mode == "updates-finish-job-state") updates_finish_job_state(ARGV[1], ARGV[2], ARGV[3]); else if (mode == "updates-fallback-job-state") updates_fallback_job_state(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5]); +else if (mode == "commit-object-sha") + commit_object_sha(); else { warn("Usage: components/updater.uc ...\n"); exit(1); diff --git a/tachyon/files/usr/lib/core/common.uc b/tachyon/files/usr/lib/core/common.uc index efa1d772..cecb622a 100644 --- a/tachyon/files/usr/lib/core/common.uc +++ b/tachyon/files/usr/lib/core/common.uc @@ -521,22 +521,22 @@ function parent_dir(path) { return slash >= 0 ? substr(path, 0, slash) : ""; } -// Read the HTTP/mixed inbound port from the generated sing-box config. -// Prefers the dedicated internal service mixed inbound (tag: service-mixed-in) -// or inbounds listening on 127.0.0.1/0.0.0.0. -// Falls back to 4534 when the config is missing or unparseable. -function get_mixed_port() { +function get_mixed_inbound_info() { let data = fs.readfile("/etc/sing-box/config.json"); - if (data == null) return 4534; + if (data == null) return null; let parsed; - try { parsed = json(data); } catch (e) { return 4534; } - if (parsed == null || parsed.inbounds == null) return 4534; + try { parsed = json(data); } catch (e) { return null; } + if (parsed == null || parsed.inbounds == null) return null; // 1. Explicitly check for the internal service mixed inbound for (let inbound in parsed.inbounds) { if (inbound.tag == "service-mixed-in" && inbound.listen_port != null) { let port = int(inbound.listen_port, 10); - if (port > 0) return port; + if (port > 0) { + let host = as_string(inbound.listen || "127.0.0.1"); + if (host == "0.0.0.0" || host == "::" || host == "") host = "127.0.0.1"; + return { host: host, port: port, tag: inbound.tag }; + } } } // 2. Check for any mixed/http inbound listening on 127.0.0.1, 0.0.0.0, or :: @@ -545,7 +545,7 @@ function get_mixed_port() { let listen = as_string(inbound.listen || ""); if (listen == "127.0.0.1" || listen == "0.0.0.0" || listen == "::" || listen == "") { let port = int(inbound.listen_port, 10); - if (port > 0) return port; + if (port > 0) return { host: "127.0.0.1", port: port, tag: inbound.tag }; } } } @@ -553,10 +553,22 @@ function get_mixed_port() { for (let inbound in parsed.inbounds) { if ((inbound.type == "mixed" || inbound.type == "http") && inbound.listen_port != null) { let port = int(inbound.listen_port, 10); - if (port > 0) return port; + if (port > 0) { + let host = as_string(inbound.listen || "127.0.0.1"); + if (host == "0.0.0.0" || host == "::" || host == "") host = "127.0.0.1"; + return { host: host, port: port, tag: inbound.tag }; + } } } - return 4534; + return null; +} + +// Prefers the dedicated internal service mixed inbound (tag: service-mixed-in) +// or inbounds listening on 127.0.0.1/0.0.0.0. +// Falls back to 4534 when the config is missing or unparseable. +function get_mixed_port() { + let info = get_mixed_inbound_info(); + return info ? info.port : 4534; } return { @@ -601,6 +613,7 @@ return { write_file, file_exists, parent_dir, + get_mixed_inbound_info, get_mixed_port, timeout_prefix, bounded_command, diff --git a/tachyon/files/usr/lib/diagnostics/runtime.uc b/tachyon/files/usr/lib/diagnostics/runtime.uc index 33766e33..2653eebd 100644 --- a/tachyon/files/usr/lib/diagnostics/runtime.uc +++ b/tachyon/files/usr/lib/diagnostics/runtime.uc @@ -902,10 +902,8 @@ function check_logs() { let rendered = status_capture([ "tachyon-logs" ], command_output_from_args([ "logread", "-l", LOGREAD_LINE_LIMIT ])); if (rendered.output != "") print(rendered.output); - if (rendered.status != 0) { - nolog("Logs not found"); - return 1; - } + else + print("No Tachyon entries found in recent system logs\n"); return 0; } @@ -917,10 +915,8 @@ function check_sing_box_logs() { let rendered = status_capture([ "matching-log-tail", "sing-box", "100" ], command_output_from_args([ "logread", "-l", LOGREAD_LINE_LIMIT ])); if (rendered.output != "") print(rendered.output); - if (rendered.status != 0) { - nolog("sing-box logs not found"); - return 1; - } + else + print("No matching logs found in system journal (needle: sing-box)\n"); return 0; } diff --git a/tachyon/files/usr/lib/diagnostics/status.uc b/tachyon/files/usr/lib/diagnostics/status.uc index c041f0c0..da76dc4b 100644 --- a/tachyon/files/usr/lib/diagnostics/status.uc +++ b/tachyon/files/usr/lib/diagnostics/status.uc @@ -1501,8 +1501,10 @@ function render_matching_log_tail(needle, max_lines) { } } - if (length(filtered) == 0) - exit(1); + if (length(filtered) == 0) { + print_line("No matching logs found in system journal (needle: " + needle + ")"); + return; + } max_lines = int(max_lines || "100", 10) || 100; let start = length(filtered) > max_lines ? length(filtered) - max_lines : 0; @@ -1535,8 +1537,10 @@ function render_tachyon_logs() { push(filtered, line); } - if (length(filtered) == 0) - exit(1); + if (length(filtered) == 0) { + print_line("No Tachyon entries found in recent system logs"); + return; + } if (start >= 0) { print_lines(filtered, start, length(filtered)); diff --git a/tachyon/files/usr/lib/service/event_controller.uc b/tachyon/files/usr/lib/service/event_controller.uc index 765bd0ea..0a8ae32d 100644 --- a/tachyon/files/usr/lib/service/event_controller.uc +++ b/tachyon/files/usr/lib/service/event_controller.uc @@ -233,6 +233,7 @@ function parse_singbox_config() { // Reads the http/mixed inbound port from the generated sing-box config. // Cached because the config only changes across a reload. let cached_proxy_port = null; +let cached_proxy_host = null; let cached_tproxy_port = null; function proxy_port() { if (cached_proxy_port !== null) @@ -241,21 +242,58 @@ function proxy_port() { // Empty when the generated config has no http/mixed inbound: probing the // default port would measure a dead listener and report the proxy broken. let port = ""; + let host = "127.0.0.1"; let sb_cfg = parse_singbox_config(); if (sb_cfg && sb_cfg.inbounds) { + // 1. Prefer service-mixed-in for (let inb in sb_cfg.inbounds) { - if (inb.type == "http" || inb.type == "mixed") { - port = as_string(inb.listen_port || 4534); + if (inb.tag == "service-mixed-in" && inb.listen_port != null) { + port = as_string(inb.listen_port); + let h = as_string(inb.listen || "127.0.0.1"); + if (h != "0.0.0.0" && h != "::" && h != "") host = h; break; } } + // 2. Localhost listeners + if (port == "") { + for (let inb in sb_cfg.inbounds) { + if ((inb.type == "http" || inb.type == "mixed") && inb.listen_port != null) { + let listen = as_string(inb.listen || ""); + if (listen == "127.0.0.1" || listen == "0.0.0.0" || listen == "::" || listen == "") { + port = as_string(inb.listen_port); + host = "127.0.0.1"; + break; + } + } + } + } + // 3. Any http or mixed inbound + if (port == "") { + for (let inb in sb_cfg.inbounds) { + if ((inb.type == "http" || inb.type == "mixed") && inb.listen_port != null) { + port = as_string(inb.listen_port); + let h = as_string(inb.listen || "127.0.0.1"); + if (h != "0.0.0.0" && h != "::" && h != "") host = h; + break; + } + } + } } cached_proxy_port = port; + cached_proxy_host = host; return port; } +function proxy_host() { + if (cached_proxy_host !== null) + return cached_proxy_host; + proxy_port(); + return cached_proxy_host || "127.0.0.1"; +} + function forget_proxy_port() { cached_proxy_port = null; + cached_proxy_host = null; cached_tproxy_port = null; sb_config_parse_reported = false; } @@ -516,13 +554,14 @@ function controller(bus, opts) { // No http/mixed inbound in the generated config: there is nothing to // measure, so the proxy must not be declared broken on a dead port. if (port == "") return; + let host = proxy_host(); let check_url = setting("ai_proxy_health_url", "https://cp.cloudflare.com/generate_204"); let started = time(); let ok = command_success_from_args([ "curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "--connect-timeout", "3", "--max-time", "5", - "--proxy", "http://127.0.0.1:" + port, + "--proxy", "http://" + host + ":" + port, check_url ]); let elapsed = (time() - started) * 1000; @@ -532,7 +571,7 @@ function controller(bus, opts) { if (ok) { state.proxy_consecutive_fails = 0; - bus.emit(EV.PROXY_UP, { port: port, ms: elapsed }); + bus.emit(EV.PROXY_UP, { host: host, port: port, ms: elapsed }); return; } @@ -549,6 +588,7 @@ function controller(bus, opts) { ]); bus.emit(EV.PROXY_DOWN, { + host: host, port: port, streak: state.proxy_consecutive_fails, direct_ok: direct_ok, @@ -838,6 +878,14 @@ function controller(bus, opts) { no_gateway = false; } + if (no_address && def_dev != null) { + let addr_out = command_capture("ip addr show " + shell_quote(def_dev) + " 2>/dev/null").output; + if (index(addr_out, "inet ") >= 0 || index(addr_out, "inet6 ") >= 0) { + no_address = false; + iface = def_dev; + } + } + // 3. Final fallback: verify that a default route exists in routing table if (no_gateway) { let route_out = command_capture("ip -4 route show default 2>/dev/null").output; diff --git a/tachyon/files/usr/lib/service/telegram.uc b/tachyon/files/usr/lib/service/telegram.uc index fe25307c..ced8db6c 100644 --- a/tachyon/files/usr/lib/service/telegram.uc +++ b/tachyon/files/usr/lib/service/telegram.uc @@ -110,19 +110,23 @@ function settings() { return object_or_empty(uci_core.get_all(CONFIG_NAME, "telegram")); } -function get_mixed_port() { +function get_mixed_proxy_info() { let settings_data = common.object_or_empty(uci_core.get_all(CONFIG_NAME, "settings")); let config_path = trim(as_string(settings_data.config_path || "")) || "/etc/sing-box/config.json"; let data = fs.readfile(config_path); - if (data == null) return 4534; + if (data == null) return null; let parsed; - try { parsed = json(data); } catch (e) { return 4534; } - if (parsed == null || parsed.inbounds == null) return 4534; + try { parsed = json(data); } catch (e) { return null; } + if (parsed == null || parsed.inbounds == null) return null; for (let inbound in parsed.inbounds) { if (inbound.tag == "service-mixed-in" && inbound.listen_port != null) { let port = int(inbound.listen_port, 10); - if (port > 0) return port; + if (port > 0) { + let host = as_string(inbound.listen || "127.0.0.1"); + if (host == "0.0.0.0" || host == "::" || host == "") host = "127.0.0.1"; + return { host: host, port: port, tag: inbound.tag }; + } } } for (let inbound in parsed.inbounds) { @@ -130,17 +134,32 @@ function get_mixed_port() { let listen = as_string(inbound.listen || ""); if (listen == "127.0.0.1" || listen == "0.0.0.0" || listen == "::" || listen == "") { let port = int(inbound.listen_port, 10); - if (port > 0) return port; + if (port > 0) return { host: "127.0.0.1", port: port, tag: inbound.tag }; } } } for (let inbound in parsed.inbounds) { - if (inbound.type == "mixed" && inbound.listen_port != null) { + if ((inbound.type == "mixed" || inbound.type == "http") && inbound.listen_port != null) { let port = int(inbound.listen_port, 10); - if (port > 0) return port; + if (port > 0) { + let host = as_string(inbound.listen || "127.0.0.1"); + if (host == "0.0.0.0" || host == "::" || host == "") host = "127.0.0.1"; + return { host: host, port: port, tag: inbound.tag }; + } } } - return 4534; + return null; +} + +function get_mixed_port() { + let info = get_mixed_proxy_info(); + return info ? info.port : 4534; +} + +function get_mixed_proxy_endpoint() { + let info = get_mixed_proxy_info(); + if (!info) return null; + return info.host + ":" + info.port; } // Liveness cache for the mixed proxy port: probing netstat on every poll @@ -184,11 +203,12 @@ function get_proxy_args() { // A running sing-box alone proves nothing: the mixed inbound may have // failed to bind while the rest of the core came up. Only route bot // traffic through 4534 when something actually listens there. + let ep = get_mixed_proxy_endpoint() || ("127.0.0.1:" + get_mixed_port()); if (!mixed_port_alive()) { if (direct_fallback_enabled()) return []; // Paranoid mode (fallback disabled): keep legacy behavior. - return [ "--proxy", "http://127.0.0.1:" + get_mixed_port() ]; + return [ "--proxy", "http://" + ep ]; } // If a specific section is configured for the bot, force-select it // through the Mihomo REST API so the mixed proxy routes bot traffic @@ -203,7 +223,7 @@ function get_proxy_args() { "http://127.0.0.1:9090/proxies/GLOBAL" ])); } - return [ "--proxy", "http://127.0.0.1:" + get_mixed_port() ]; + return [ "--proxy", "http://" + ep ]; } if (cfg.fallback_socks && trim(cfg.fallback_socks) != "") { return [ "--proxy", "socks5h://" + trim(cfg.fallback_socks) ]; @@ -1470,7 +1490,7 @@ function exec_support_bundle(token, chat_id) { } function exec_close_connections(token, chat_id) { - let out = command_capture(command_from_args(["curl", "-s", "-X", "DELETE", "http://127.0.0.1:" + get_mixed_port() + "/connections"])); + let out = command_capture(command_from_args(["curl", "-s", "-X", "DELETE", "http://127.0.0.1:9090/connections"])); if (out && out.status == 0) send_message(token, chat_id, "✅ " + t("conn_closed_msg") + "", "HTML", [[{text:"⬅️ " + t("nav_menu"), callback_data:"/menu"}]]); else @@ -1561,7 +1581,7 @@ function exec_check_updates(token, chat_id, msg_id) { } function view_instances(token, chat_id, msg_id) { - let res = command_capture(command_from_args(["curl", "-s", "http://127.0.0.1:" + get_mixed_port() + "/proxies"])); + let res = command_capture(command_from_args(["curl", "-s", "http://127.0.0.1:9090/proxies"])); let text = "🖧 Live Server Instances\n\n"; if (res && res.status == 0 && res.output) { try { @@ -2922,7 +2942,7 @@ function send_daily_digest(token, admin_ids) { let up = m ? m[1] : t("status_unknown"); text += "⏱ " + t("daily_uptime") + ": " + up + "\n"; - let res = command_capture(command_from_args(["curl", "-s", "http://127.0.0.1:" + get_mixed_port() + "/traffic"])); + let res = command_capture(command_from_args(["curl", "-s", "http://127.0.0.1:9090/traffic"])); if (res && res.status == 0 && res.output) { try { let tr = json(res.output); @@ -3273,19 +3293,28 @@ function diagnose() { push(checks, { name: "sing_box", ok: sb_running, - message: sb_running ? "sing-box is running" : "sing-box is NOT running — bot cannot route through proxy" + message: sb_running ? "sing-box is running" : "sing-box is NOT running — proxy routing unavailable" }); - if (!sb_running) ok = false; // 4. Mixed proxy port - let mixed_port = get_mixed_port(); - let port_check = command_capture("netstat -tlnp 2>/dev/null | grep ':" + mixed_port + " '"); - let port_ok = port_check && port_check.status === 0 && trim(port_check.output || "") !== ""; - push(checks, { - name: "proxy_port", - ok: port_ok, - message: port_ok ? "Mixed proxy port " + mixed_port + " is listening" : "Port " + mixed_port + " not listening — proxy may not be available" - }); + let proxy_info = get_mixed_proxy_info(); + let proxy_ep = proxy_info ? (proxy_info.host + ":" + proxy_info.port) : null; + let port_ok = false; + if (proxy_info) { + let port_check = command_capture("netstat -tlnp 2>/dev/null | grep ':" + proxy_info.port + " '"); + port_ok = port_check && port_check.status === 0 && trim(port_check.output || "") !== ""; + push(checks, { + name: "proxy_port", + ok: port_ok, + message: port_ok ? "Mixed proxy port " + proxy_info.port + " is listening (" + proxy_info.host + ")" : "Port " + proxy_info.port + " not listening — proxy may not be available" + }); + } else { + push(checks, { + name: "proxy_port", + ok: true, + message: "Mixed proxy is not configured in sing-box — bot will use direct connection" + }); + } // 5. DNS resolution let dns_check = command_capture("nslookup api.telegram.org 2>/dev/null"); @@ -3301,52 +3330,54 @@ function diagnose() { }); // 6. Direct API test (IPv4) + let d_ok = false; if (cfg.bot_token) { let direct = command_capture(command_from_args([ "curl", "-s", "-4", "--connect-timeout", "5", "--max-time", "8", "https://api.telegram.org/bot" + cfg.bot_token + "/getMe" ])); let d_out = (direct && direct.output) || ""; - let d_ok = direct && direct.status === 0 && index(d_out, '"ok":true') >= 0; + d_ok = direct && direct.status === 0 && index(d_out, '"ok":true') >= 0; let d_msg = d_ok ? "Direct IPv4 to Telegram API works" : (index(d_out, '"ok":false') >= 0 ? "API returned error — token may be invalid" : "Cannot reach Telegram API directly (IPv4 may be blocked by ISP)"); push(checks, { name: "direct_api", ok: d_ok, message: d_msg }); - if (!d_ok) ok = false; } // 7. Proxy API test - if (cfg.bot_token && sb_running) { + let p_ok = false; + if (cfg.bot_token && sb_running && proxy_info && port_ok) { let proxied = command_capture(command_from_args([ "curl", "-s", "-4", "--connect-timeout", "5", "--max-time", "8", - "--proxy", "http://127.0.0.1:" + mixed_port, + "--proxy", "http://" + proxy_ep, "https://api.telegram.org/bot" + cfg.bot_token + "/getMe" ])); let p_out = (proxied && proxied.output) || ""; - let p_ok = proxied && proxied.status === 0 && index(p_out, '"ok":true') >= 0; + p_ok = proxied && proxied.status === 0 && index(p_out, '"ok":true') >= 0; push(checks, { name: "proxy_api", ok: p_ok, - message: p_ok ? "Proxy route to Telegram API works" : "Proxy route failed — check sing-box outbound for Telegram" + message: p_ok ? "Proxy route (" + proxy_ep + ") to Telegram API works" : "Proxy route failed — check sing-box outbound for Telegram" + }); + } else if (proxy_info && !port_ok) { + push(checks, { + name: "proxy_api", + ok: false, + message: "Proxy route skipped — mixed proxy port " + proxy_info.port + " is not listening" }); - if (!p_ok) ok = false; } + // Route check: at least one connection method must work + let route_ok = d_ok || p_ok; + if (!route_ok) ok = false; + // 8. Send test message if (cfg.bot_token && cfg.admin_ids) { let first_admin = trim(split(cfg.admin_ids, /,/)[0] || ""); if (first_admin !== "") { - let send_res = command_capture(command_from_args([ - "curl", "-s", "-m", "10", "--connect-timeout", "5", - "--proxy", "http://127.0.0.1:" + mixed_port, - "-X", "POST", "-H", "Content-Type: application/json", - "-d", sprintf("%J", { chat_id: int(first_admin), text: "\u2705 Tachyon connection test passed" }), - "https://api.telegram.org/bot" + cfg.bot_token + "/sendMessage" - ])); - let s_out = (send_res && send_res.output) || ""; - let s_ok = send_res && send_res.status === 0 && index(s_out, '"ok":true') >= 0; + let s_ok = send_message(cfg.bot_token, first_admin, "✅ Tachyon connection test passed"); push(checks, { name: "send_message", ok: s_ok, diff --git a/tachyon/files/usr/lib/service/watchdog.uc b/tachyon/files/usr/lib/service/watchdog.uc index eaa15e8f..3fe80990 100644 --- a/tachyon/files/usr/lib/service/watchdog.uc +++ b/tachyon/files/usr/lib/service/watchdog.uc @@ -848,7 +848,7 @@ function heal_proxy_connectivity(ev) { if (int(ev.payload.streak) < 2) return; if (suppressed_by_root_cause("heal_proxy_connectivity")) return; - let proxy_addr = "127.0.0.1:" + as_string(ev.payload.port); + let proxy_addr = (ev.payload.host ? ev.payload.host : "127.0.0.1") + ":" + as_string(ev.payload.port); let incident = { type: "proxy", description: "Зависание или неполный отклик прокси-порту sing-box (" + proxy_addr + ")", @@ -932,9 +932,11 @@ function heal_tproxy_port(ev) { function heal_wan_and_gateway(ev) { if (settings().recovery_bypass == "1") return; + if (settings().ai_heal_wan_enabled == "0") return; + let tcfg = common.object_or_empty(uci_core.get_all(CONFIG_NAME, "telegram")); if (tcfg.notify_crash != "0") { - send_telegram_notification("⚠️ *Watchdog:* WAN/Gateway проблема. Перезапуск wan...", "heal_wan_and_gateway", 600); + send_telegram_notification("⚠️ *Watchdog:* WAN/Gateway проблема. Попытка обновления сетевого интерфейса...", "heal_wan_and_gateway", 600); } // Claimed before the interface goes down, not after: ifdown itself is what // makes the proxy and DNS probes fail, and those facts arrive while ifup is @@ -942,16 +944,10 @@ function heal_wan_and_gateway(ev) { wan_repair_until = time() + SUPPRESSION_DEADLINE; // The address is still there but the default route vanished: a graceful // renew re-applies the routes without dropping the interface (issue #31). - // Only when the address itself is gone does the interface get recycled, - // and even then only if the renew did not restore the route within 5s. - let cmd; - if (ev.payload.no_address) { - cmd = "/sbin/ifdown wan >/dev/null 2>&1 && /sbin/ifup wan >/dev/null 2>&1"; - } else { - cmd = "/sbin/ubus call network.interface.wan renew >/dev/null 2>&1; sleep 5; " + - "ip route 2>/dev/null | grep -q default || " + - "(/sbin/ifdown wan >/dev/null 2>&1 && /sbin/ifup wan >/dev/null 2>&1)"; - } + // Always attempt graceful renew first before falling back to ifdown/ifup. + let cmd = "/sbin/ubus call network.interface.wan renew >/dev/null 2>&1; sleep 6; " + + "(ip -4 route show default 2>/dev/null | grep -q default || " + + "(/sbin/ifdown wan >/dev/null 2>&1 && /sbin/ifup wan >/dev/null 2>&1))"; bg_system(cmd); } diff --git a/tachyon/files/usr/lib/singbox/runtime.uc b/tachyon/files/usr/lib/singbox/runtime.uc index 7c92b3a0..353b8d0d 100644 --- a/tachyon/files/usr/lib/singbox/runtime.uc +++ b/tachyon/files/usr/lib/singbox/runtime.uc @@ -606,7 +606,7 @@ function network_interface_dump_first_private_ipv4() { function service_listen_address_value(settings) { let configured = option(settings, "service_listen_address", ""); if (configured != "") { - log_message("service_listen_address is set manually; automatic listen-address detection is skipped", "warn"); + log_message("service_listen_address is set manually; automatic listen-address detection is skipped", "debug"); return configured; }