fix(updates): resolve tag commit SHA and format build fingerprints; fix telegram, watchdog and logs

This commit is contained in:
Dushnilin
2026-09-05 23:52:16 +03:00
parent 916e9c0e6a
commit 11925e7ec5
15 changed files with 260 additions and 91 deletions
+3
View File
@@ -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' }}
+3
View File
@@ -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 "новая сборка"
@@ -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) : '';
+1 -1
View File
@@ -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
@@ -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;
+3
View File
@@ -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 "новая сборка"
+66 -4
View File
@@ -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 + ")");
+11 -1
View File
@@ -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 <operation> ...\n");
exit(1);
+25 -12
View File
@@ -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,
+4 -8
View File
@@ -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;
}
+8 -4
View File
@@ -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));
@@ -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;
+67 -36
View File
@@ -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, "✅ <b>" + t("conn_closed_msg") + "</b>", "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 = "🖧 <b>Live Server Instances</b>\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 || "") !== "";
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 " + mixed_port + " is listening" : "Port " + mixed_port + " not listening — proxy may not be available"
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,
+8 -12
View File
@@ -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);
}
+1 -1
View File
@@ -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;
}