v2.5.0: show routed services behind port 443

This commit is contained in:
Виталий Литвинов
2026-04-25 13:25:32 +03:00
parent 5225811b3c
commit 7eaeef8b49
3 changed files with 106 additions and 13 deletions

View File

@@ -376,7 +376,7 @@ def parse_ss_listeners(output: str, proto: str, port: int = 443) -> list[dict[st
return listeners 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]] = [] listeners: list[dict[str, Any]] = []
errors: list[str] = [] errors: list[str] = []
for proto, args in { for proto, args in {
@@ -385,14 +385,75 @@ def port_443_status() -> dict[str, Any]:
}.items(): }.items():
code, stdout, stderr = run(args, timeout=2) code, stdout, stderr = run(args, timeout=2)
if code == 0: if code == 0:
listeners.extend(parse_ss_listeners(stdout, proto, 443)) listeners.extend(parse_ss_listeners(stdout, proto, port))
elif stderr.strip(): elif stderr.strip():
errors.append(stderr.strip()) errors.append(stderr.strip())
listeners.sort(key=lambda item: (item["proto"], item["address"], item["process"])) 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 { return {
"checked_at": int(time.time()), "checked_at": int(time.time()),
"configured_port": read_telemt_port(), "configured_port": read_telemt_port(),
"listeners": listeners, "listeners": listeners,
"routes": routed_behind_443(),
"ok": not errors, "ok": not errors,
"error": "; ".join(errors[:2]), "error": "; ".join(errors[:2]),
} }

View File

@@ -136,14 +136,19 @@ const i18n = {
languageSaved: "Language saved", languageSaved: "Language saved",
keyEnabled: "Key enabled", keyEnabled: "Key enabled",
keyDisabled: "Key disabled", keyDisabled: "Key disabled",
visualTitle: "Port 443 listeners", visualTitle: "Port 443 map",
visualText: "Actual TCP/UDP listeners on public port 443: telemt, website, Xray/3x-ui, AmneziaWG or another service.", visualText: "Shows the public 443 listener and services routed behind it, including the website on local nginx.",
port443Checked: "checked", port443Checked: "checked",
port443NoListeners: "No 443 listeners found", port443NoListeners: "No 443 listeners found",
port443Listeners: "listeners", port443Listeners: "listeners",
port443Routes: "routed",
port443Error: "Port check failed", port443Error: "Port check failed",
port443Public: "public", port443Public: "public",
port443Configured: "telemt: {port}", port443Configured: "telemt: {port}",
port443PublicSection: "Public 443",
port443BehindSection: "Behind 443",
port443NoRoutes: "No routed services detected",
port443Via: "via {value}",
roleMtproxy: "MTProxy", roleMtproxy: "MTProxy",
roleSite: "Website", roleSite: "Website",
roleXray: "Xray / 3x-ui", roleXray: "Xray / 3x-ui",
@@ -318,14 +323,19 @@ const i18n = {
languageSaved: "Язык сохранён", languageSaved: "Язык сохранён",
keyEnabled: "Ключ включён", keyEnabled: "Ключ включён",
keyDisabled: "Ключ отключён", keyDisabled: "Ключ отключён",
visualTitle: "Кто слушает порт 443", visualTitle: "Карта порта 443",
visualText: "Реальные TCP/UDP-процессы на публичном 443: telemt, сайт, Xray/3x-ui, AmneziaWG или другой сервис.", visualText: "Показывает публичного слушателя 443 и сервисы, которые живут за ним, включая сайт на локальном nginx.",
port443Checked: "проверено", port443Checked: "проверено",
port443NoListeners: "Слушателей 443 не найдено", port443NoListeners: "Слушателей 443 не найдено",
port443Listeners: "слушателей", port443Listeners: "слушателей",
port443Routes: "за 443",
port443Error: "Проверка порта не удалась", port443Error: "Проверка порта не удалась",
port443Public: "публичный", port443Public: "публичный",
port443Configured: "telemt: {port}", port443Configured: "telemt: {port}",
port443PublicSection: "Публичный 443",
port443BehindSection: "За портом 443",
port443NoRoutes: "Маршрутизируемых сервисов не найдено",
port443Via: "через {value}",
roleMtproxy: "MTProxy", roleMtproxy: "MTProxy",
roleSite: "Сайт", roleSite: "Сайт",
roleXray: "Xray / 3x-ui", roleXray: "Xray / 3x-ui",
@@ -637,6 +647,7 @@ function roleLabel(role) {
function renderPort443(payload = {}) { function renderPort443(payload = {}) {
const listeners = Array.isArray(payload.listeners) ? payload.listeners : []; const listeners = Array.isArray(payload.listeners) ? payload.listeners : [];
const routes = Array.isArray(payload.routes) ? payload.routes : [];
const summary = $("#port443Summary"); const summary = $("#port443Summary");
const list = $("#port443List"); const list = $("#port443List");
const configuredPort = Number(payload.configured_port) || 443; const configuredPort = Number(payload.configured_port) || 443;
@@ -649,14 +660,10 @@ function renderPort443(payload = {}) {
summary.textContent = t("port443NoListeners"); summary.textContent = t("port443NoListeners");
summary.className = "port-status warn"; summary.className = "port-status warn";
} else { } else {
summary.textContent = `${listeners.length} ${t("port443Listeners")}`; summary.textContent = `${listeners.length} ${t("port443Listeners")}${routes.length ? ` · ${routes.length} ${t("port443Routes")}` : ""}`;
summary.className = "port-status ok"; summary.className = "port-status ok";
} }
if (!listeners.length) { const listenerHtml = listeners.length ? listeners.map((item) => {
list.innerHTML = `<div class="port-empty">${escapeHtml(payload.error || t("port443NoListeners"))}</div>`;
return;
}
list.innerHTML = listeners.map((item) => {
const title = `${item.proto || ""} ${item.address || ""} · ${item.process || "unknown"}${item.pid ? ` · pid ${item.pid}` : ""}`; const title = `${item.proto || ""} ${item.address || ""} · ${item.process || "unknown"}${item.pid ? ` · pid ${item.pid}` : ""}`;
return `<article class="port-listener role-${escapeAttr(item.role || "other")}" title="${escapeAttr(title)}"> return `<article class="port-listener role-${escapeAttr(item.role || "other")}" title="${escapeAttr(title)}">
<div> <div>
@@ -665,7 +672,24 @@ function renderPort443(payload = {}) {
</div> </div>
<small>${escapeHtml(item.proto || "--")} · ${escapeHtml(item.address || "--")}</small> <small>${escapeHtml(item.proto || "--")} · ${escapeHtml(item.address || "--")}</small>
</article>`; </article>`;
}).join(""); }).join("") : `<div class="port-empty">${escapeHtml(payload.error || t("port443NoListeners"))}</div>`;
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 `<article class="port-listener role-${escapeAttr(item.role || "other")}" title="${escapeAttr(title)}">
<div>
<strong>${escapeHtml(roleLabel(item.role))}</strong>
<span>${escapeHtml(item.process || "unknown")}${item.status ? ` · ${escapeHtml(statusLabel(item.status))}` : ""}</span>
</div>
<small>${escapeHtml(item.public || "--")}${escapeHtml(item.target || "--")}${via ? ` · ${escapeHtml(via)}` : ""}</small>
</article>`;
}).join("") : `<div class="port-empty">${escapeHtml(t("port443NoRoutes"))}</div>`;
list.innerHTML = `
<div class="port-section-label">${escapeHtml(t("port443PublicSection"))}</div>
${listenerHtml}
<div class="port-section-label">${escapeHtml(t("port443BehindSection"))}</div>
${routeHtml}
`;
} }
function renderOverview() { function renderOverview() {

View File

@@ -400,6 +400,14 @@ h2 {
gap: 8px; gap: 8px;
} }
.port-section-label {
margin-top: 2px;
color: var(--muted);
font-size: 11px;
font-weight: 900;
text-transform: uppercase;
}
.port-listener, .port-listener,
.port-empty { .port-empty {
display: grid; display: grid;