From cc31234e91e52fb3ec375aa08013cc0bee3b7ab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=91=D0=BE=D0=B1?= =?UTF-8?q?=D1=8B=D1=80=D0=B5=D0=B2?= Date: Mon, 27 Apr 2026 21:54:34 +0300 Subject: [PATCH] fix(tunnel): install cronie, start crond, killall watchdog; add tunnel-status endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - opkg install now includes cronie so crond is actually present - /opt/etc/init.d/S10crond start runs after install so cron watchdog fires - cron watchdog switches from pgrep (not always in PATH) to killall -0 autossh - killall autossh instead of pkill before re-launching (works in BusyBox) - GET /api/routers/{rid}/tunnel-status: checks if tunnel port is listening on VPS localhost - "⟳ Проверить связь" button in tunnel modal calls the new endpoint Co-Authored-By: Claude Sonnet 4.6 --- app/main.py | 35 ++++++++++++++++++++++++++++++----- templates/index.html | 14 ++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/app/main.py b/app/main.py index 0f57d11..e948278 100644 --- a/app/main.py +++ b/app/main.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio import logging +import socket from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI, Header, HTTPException, Query @@ -293,8 +294,9 @@ async def tunnel_cmd(rid: str, x_admin_password: str = Header("")): cmd = ( f"export PATH=\"/opt/bin:/opt/sbin:/bin:/sbin:/usr/bin:/usr/sbin:$PATH\"\n\n" - f"# Установить зависимости\n" - f"opkg install autossh sshpass 2>/dev/null; true\n\n" + f"# Установить зависимости (autossh, sshpass, cronie для watchdog)\n" + f"opkg install autossh sshpass cronie 2>/dev/null; true\n" + f"/opt/etc/init.d/S10crond start 2>/dev/null; true\n\n" f"# Создать скрипт тоннеля\n" f"cat > /opt/bin/kdns_tunnel.sh << 'ENDSCRIPT'\n" f"#!/bin/sh\n" @@ -305,11 +307,11 @@ async def tunnel_cmd(rid: str, x_admin_password: str = Header("")): f" -N -R {port}:localhost:81 {vps_user}@{vps_host} -p {vps_port}\n" f"ENDSCRIPT\n" f"chmod +x /opt/bin/kdns_tunnel.sh\n\n" - f"# Добавить в cron (запуск если не работает, каждые 3 мин)\n" + f"# Watchdog через cron: перезапуск каждые 3 мин если не работает\n" f"(crontab -l 2>/dev/null | grep -v kdns_tunnel; " - f"echo '*/3 * * * * pgrep -f kdns_tunnel.sh || /opt/bin/kdns_tunnel.sh &') | crontab -\n\n" + f"echo '*/3 * * * * killall -0 autossh 2>/dev/null || /opt/bin/kdns_tunnel.sh &') | crontab -\n\n" f"# Запустить сейчас\n" - f"pkill -f kdns_tunnel.sh 2>/dev/null; sleep 1\n" + f"killall autossh 2>/dev/null; sleep 1\n" f"nohup /opt/bin/kdns_tunnel.sh >/dev/null 2>&1 &\n\n" f"echo \"Тоннель запущен: порт 81 → VPS:{port}\"\n" f"echo \"URL для платформы: http://localhost:{port}\"" @@ -339,6 +341,29 @@ async def tunnel_remove(rid: str, x_admin_password: str = Header("")): return {"ok": True} +@app.get("/api/routers/{rid}/tunnel-status") +async def tunnel_status(rid: str, x_admin_password: str = Header("")): + """Проверить: слушает ли тоннельный порт на localhost VPS прямо сейчас.""" + _chk(x_admin_password) + cur = load_store() + r = next((x for x in cur.get("routers") or [] if x.get("id") == rid), None) + if not r: + raise HTTPException(404, "Роутер не найден") + port = r.get("tunnel_port") + if not port: + return {"active": False, "reason": "tunnel_port не назначен"} + + def _check() -> bool: + try: + with socket.create_connection(("127.0.0.1", int(port)), timeout=2): + return True + except OSError: + return False + + active = await asyncio.to_thread(_check) + return {"active": active, "tunnel_port": port} + + @app.post("/api/test-router/{rid}") async def test_router(rid: str, x_admin_password: str = Header("")): _chk(x_admin_password) diff --git a/templates/index.html b/templates/index.html index 9a9fdd1..bc9d2d3 100644 --- a/templates/index.html +++ b/templates/index.html @@ -163,6 +163,7 @@
+
@@ -453,6 +454,19 @@ async function applyTunnelUrl(){ msg.style.color='var(--er)';msg.textContent='Ошибка: '+(j.detail||r.statusText);msg.style.display='block'; } } +async function checkTunnelStatus(){ + if(!_tunnelRouterId)return; + const msg=document.getElementById('tunnel-msg'); + msg.style.color='var(--mu)';msg.textContent='Проверяю порт…';msg.style.display='block'; + const r=await fetch(`/api/routers/${encodeURIComponent(_tunnelRouterId)}/tunnel-status`,{headers:hdr()}); + if(r.status===401){logout();return;} + const j=await r.json().catch(()=>({})); + if(j.active){ + msg.style.color='var(--ok)';msg.textContent=`✓ Тоннель активен — порт ${j.tunnel_port} слушает`; + } else { + msg.style.color='var(--er)';msg.textContent=`✗ Порт ${j.tunnel_port||'?'} не отвечает — запусти команду на роутере`; + } +} async function removeTunnel(){ if(!_tunnelRouterId)return; const msg=document.getElementById('tunnel-msg');