From 7eaeef8b49a7ed643472105f059ed348a960dc49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B8?= =?UTF-8?q?=D1=82=D0=B2=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Sat, 25 Apr 2026 13:25:32 +0300 Subject: [PATCH] v2.5.0: show routed services behind port 443 --- admin-web/server.py | 65 +++++++++++++++++++++++++++++++++++-- admin-web/static/app.js | 46 +++++++++++++++++++------- admin-web/static/styles.css | 8 +++++ 3 files changed, 106 insertions(+), 13 deletions(-) diff --git a/admin-web/server.py b/admin-web/server.py index 31c46d6..0db0082 100644 --- a/admin-web/server.py +++ b/admin-web/server.py @@ -376,7 +376,7 @@ def parse_ss_listeners(output: str, proto: str, port: int = 443) -> list[dict[st return listeners -def port_443_status() -> dict[str, Any]: +def collect_port_listeners(port: int) -> tuple[list[dict[str, Any]], list[str]]: listeners: list[dict[str, Any]] = [] errors: list[str] = [] for proto, args in { @@ -385,14 +385,75 @@ def port_443_status() -> dict[str, Any]: }.items(): code, stdout, stderr = run(args, timeout=2) if code == 0: - listeners.extend(parse_ss_listeners(stdout, proto, 443)) + listeners.extend(parse_ss_listeners(stdout, proto, port)) elif stderr.strip(): errors.append(stderr.strip()) listeners.sort(key=lambda item: (item["proto"], item["address"], item["process"])) + return listeners, errors + + +def read_telemt_edge_settings() -> dict[str, Any]: + settings: dict[str, Any] = {"tls_domain": "", "mask_port": 0, "dns_overrides": []} + if not TELEMT_CONFIG.exists(): + return settings + section = "" + for raw in TELEMT_CONFIG.read_text(encoding="utf-8", errors="ignore").splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + if line.startswith("[") and line.endswith("]"): + section = line.strip("[]") + continue + if "=" not in line: + continue + key, value = line.split("=", 1) + key = key.strip() + value = value.strip().split("#", 1)[0].strip() + if section == "censorship" and key == "tls_domain": + settings["tls_domain"] = value.strip('"').strip("'") + elif section == "censorship" and key == "mask_port": + try: + settings["mask_port"] = int(value) + except ValueError: + settings["mask_port"] = 0 + elif section == "network" and key == "dns_overrides": + settings["dns_overrides"] = re.findall(r'"([^"]+)"', value) + return settings + + +def routed_behind_443() -> list[dict[str, Any]]: + config = load_json(GOTELEGRAM_CONFIG, {}) or {} + mode = str(config.get("mode") or "") + domain = str(config.get("domain") or "") + settings = read_telemt_edge_settings() + mask_port = int(settings.get("mask_port") or 0) + tls_domain = str(settings.get("tls_domain") or domain) + routes: list[dict[str, Any]] = [] + if mode == "pro" and domain and mask_port and mask_port != 443: + internal, _ = collect_port_listeners(mask_port) + site_listener = next((item for item in internal if item.get("role") == "site"), None) + routes.append({ + "role": "site", + "proto": "HTTPS", + "public": f"{domain}:443", + "target": f"127.0.0.1:{mask_port}", + "process": (site_listener or {}).get("process") or "nginx", + "pid": (site_listener or {}).get("pid") or "", + "status": service_status("nginx"), + "via": "telemt dns_overrides", + "tls_domain": tls_domain, + "details": settings.get("dns_overrides") or [], + }) + return routes + + +def port_443_status() -> dict[str, Any]: + listeners, errors = collect_port_listeners(443) return { "checked_at": int(time.time()), "configured_port": read_telemt_port(), "listeners": listeners, + "routes": routed_behind_443(), "ok": not errors, "error": "; ".join(errors[:2]), } diff --git a/admin-web/static/app.js b/admin-web/static/app.js index 6324fc2..171f33f 100644 --- a/admin-web/static/app.js +++ b/admin-web/static/app.js @@ -136,14 +136,19 @@ const i18n = { languageSaved: "Language saved", keyEnabled: "Key enabled", keyDisabled: "Key disabled", - visualTitle: "Port 443 listeners", - visualText: "Actual TCP/UDP listeners on public port 443: telemt, website, Xray/3x-ui, AmneziaWG or another service.", + visualTitle: "Port 443 map", + visualText: "Shows the public 443 listener and services routed behind it, including the website on local nginx.", port443Checked: "checked", port443NoListeners: "No 443 listeners found", port443Listeners: "listeners", + port443Routes: "routed", port443Error: "Port check failed", port443Public: "public", port443Configured: "telemt: {port}", + port443PublicSection: "Public 443", + port443BehindSection: "Behind 443", + port443NoRoutes: "No routed services detected", + port443Via: "via {value}", roleMtproxy: "MTProxy", roleSite: "Website", roleXray: "Xray / 3x-ui", @@ -318,14 +323,19 @@ const i18n = { languageSaved: "Язык сохранён", keyEnabled: "Ключ включён", keyDisabled: "Ключ отключён", - visualTitle: "Кто слушает порт 443", - visualText: "Реальные TCP/UDP-процессы на публичном 443: telemt, сайт, Xray/3x-ui, AmneziaWG или другой сервис.", + visualTitle: "Карта порта 443", + visualText: "Показывает публичного слушателя 443 и сервисы, которые живут за ним, включая сайт на локальном nginx.", port443Checked: "проверено", port443NoListeners: "Слушателей 443 не найдено", port443Listeners: "слушателей", + port443Routes: "за 443", port443Error: "Проверка порта не удалась", port443Public: "публичный", port443Configured: "telemt: {port}", + port443PublicSection: "Публичный 443", + port443BehindSection: "За портом 443", + port443NoRoutes: "Маршрутизируемых сервисов не найдено", + port443Via: "через {value}", roleMtproxy: "MTProxy", roleSite: "Сайт", roleXray: "Xray / 3x-ui", @@ -637,6 +647,7 @@ function roleLabel(role) { function renderPort443(payload = {}) { const listeners = Array.isArray(payload.listeners) ? payload.listeners : []; + const routes = Array.isArray(payload.routes) ? payload.routes : []; const summary = $("#port443Summary"); const list = $("#port443List"); const configuredPort = Number(payload.configured_port) || 443; @@ -649,14 +660,10 @@ function renderPort443(payload = {}) { summary.textContent = t("port443NoListeners"); summary.className = "port-status warn"; } else { - summary.textContent = `${listeners.length} ${t("port443Listeners")}`; + summary.textContent = `${listeners.length} ${t("port443Listeners")}${routes.length ? ` · ${routes.length} ${t("port443Routes")}` : ""}`; summary.className = "port-status ok"; } - if (!listeners.length) { - list.innerHTML = `
${escapeHtml(payload.error || t("port443NoListeners"))}
`; - return; - } - list.innerHTML = listeners.map((item) => { + const listenerHtml = listeners.length ? listeners.map((item) => { const title = `${item.proto || ""} ${item.address || ""} · ${item.process || "unknown"}${item.pid ? ` · pid ${item.pid}` : ""}`; return `
@@ -665,7 +672,24 @@ function renderPort443(payload = {}) {
${escapeHtml(item.proto || "--")} · ${escapeHtml(item.address || "--")}
`; - }).join(""); + }).join("") : `
${escapeHtml(payload.error || t("port443NoListeners"))}
`; + const routeHtml = routes.length ? routes.map((item) => { + const via = item.via ? t("port443Via").replace("{value}", item.via) : ""; + const title = `${item.public || ""} → ${item.target || ""} · ${item.process || ""}`; + return `
+
+ ${escapeHtml(roleLabel(item.role))} + ${escapeHtml(item.process || "unknown")}${item.status ? ` · ${escapeHtml(statusLabel(item.status))}` : ""} +
+ ${escapeHtml(item.public || "--")} → ${escapeHtml(item.target || "--")}${via ? ` · ${escapeHtml(via)}` : ""} +
`; + }).join("") : `
${escapeHtml(t("port443NoRoutes"))}
`; + list.innerHTML = ` +
${escapeHtml(t("port443PublicSection"))}
+ ${listenerHtml} +
${escapeHtml(t("port443BehindSection"))}
+ ${routeHtml} + `; } function renderOverview() { diff --git a/admin-web/static/styles.css b/admin-web/static/styles.css index 9e32e65..6d41ef3 100644 --- a/admin-web/static/styles.css +++ b/admin-web/static/styles.css @@ -400,6 +400,14 @@ h2 { gap: 8px; } +.port-section-label { + margin-top: 2px; + color: var(--muted); + font-size: 11px; + font-weight: 900; + text-transform: uppercase; +} + .port-listener, .port-empty { display: grid;