fix(updates): resolve tag commit SHA and format build fingerprints; fix telegram, watchdog and logs
This commit is contained in:
@@ -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' }}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 "новая сборка"
|
||||
|
||||
@@ -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 + ")");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user