Amnezia-style SSH server connect with verify, ping and server_info

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-25 21:30:43 +03:00
co-authored by Cursor
parent 088f02cdbe
commit 321a5b2504
9 changed files with 334 additions and 40 deletions
+61
View File
@@ -1,5 +1,7 @@
from __future__ import annotations
import asyncio
from datetime import datetime, timezone
from pathlib import Path
from sqlalchemy import func, select
@@ -16,6 +18,7 @@ from app.services.crypto import (
render_server_config,
server_address,
)
from app.services.ssh import probe_tcp, verify_ssh
async def ensure_default_servers(session: AsyncSession) -> None:
@@ -137,6 +140,14 @@ async def create_server(
if not username:
raise ValueError("Укажите SSH логин")
# Like Amnezia-Web-Panel: verify SSH before saving
try:
server_info = await asyncio.to_thread(
verify_ssh, host, ssh_port or 22, username, password, private_key
)
except Exception as exc: # noqa: BLE001
raise ValueError(f"Подключение не удалось: {exc}") from exc
defaults = _defaults_for_protocol(protocol)
keys = generate_keypair()
# Unique interface per server id is assigned after insert; use temp then update
@@ -150,6 +161,7 @@ async def create_server(
raise ValueError("Слишком много серверов для авто-подсети")
subnet = f"10.{octet}.0.0/24"
now = datetime.now(timezone.utc)
server = VpnServer(
name=name.strip() or host,
protocol=protocol,
@@ -165,6 +177,9 @@ async def create_server(
ssh_username=username,
ssh_password=password,
ssh_private_key=private_key,
ssh_server_info=server_info,
ssh_reachable=True,
ssh_last_checked=now,
jc=defaults["jc"],
jmin=defaults["jmin"],
jmax=defaults["jmax"],
@@ -183,6 +198,52 @@ async def create_server(
return server
async def check_server_ssh(session: AsyncSession, server_id: int) -> VpnServer:
server = await session.get(VpnServer, server_id)
if not server:
raise ValueError("Server not found")
if not server.ssh_host or not server.ssh_username:
raise ValueError("SSH данные не заданы")
if not server.ssh_password and not server.ssh_private_key:
raise ValueError("Нет пароля и ключа SSH")
now = datetime.now(timezone.utc)
try:
info = await asyncio.to_thread(
verify_ssh,
server.ssh_host,
server.ssh_port or 22,
server.ssh_username,
server.ssh_password,
server.ssh_private_key,
)
server.ssh_server_info = info
server.ssh_reachable = True
server.ssh_last_checked = now
except Exception as exc: # noqa: BLE001
server.ssh_reachable = False
server.ssh_last_checked = now
await session.commit()
raise ValueError(f"Подключение не удалось: {exc}") from exc
await session.commit()
await session.refresh(server)
return server
async def probe_servers(servers: list[VpnServer]) -> dict[int, dict]:
"""TCP ping SSH ports for UI online indicators."""
async def one(s: VpnServer) -> tuple[int, dict]:
if not s.ssh_host:
return s.id, {"online": False, "latency_ms": None}
result = await asyncio.to_thread(probe_tcp, s.ssh_host, s.ssh_port or 22)
return s.id, {"online": result.online, "latency_ms": result.latency_ms}
pairs = await asyncio.gather(*(one(s) for s in servers))
return dict(pairs)
async def update_server_settings(
session: AsyncSession,
server_id: int,