Initial commit: Debian Manager — управление Debian-серверами

- SSH/SFTP управление без агентов
- Пакеты (apt), Docker, файлы, systemd, ufw, cron
- Плейбуки, групповые операции, планировщик
- Бэкапы, отчёты, Portainer, Proxmox VE, Gitea Deploy
- Терминал, Telegram-уведомления, тёмная/светлая тема
This commit is contained in:
dinlo
2026-09-05 19:46:45 +08:00
commit f624fee5e0
65 changed files with 6981 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
"""UFW файрвол."""
from __future__ import annotations
from dataclasses import dataclass
from typing import List, Optional
from app.core.ssh import SSHClient
@dataclass
class Rule:
number: int
to: str
action: str
from_: str
raw: str
async def is_installed(c: SSHClient) -> bool:
r = await c.run("which ufw")
return r.ok and bool(r.stdout.strip())
async def status(c: SSHClient) -> tuple[bool, str]:
"""(active, raw_status)"""
r = await c.sudo("ufw status verbose", timeout=30)
active = "Status: active" in r.stdout
return active, r.stdout
async def list_rules(c: SSHClient) -> List[Rule]:
r = await c.sudo("ufw status numbered", timeout=30)
rules: List[Rule] = []
for line in r.stdout.splitlines():
line = line.strip()
if line.startswith("[") and "]" in line:
num = int(line[1:line.index("]")].strip())
rest = line[line.index("]") + 1:].strip()
parts = rest.split()
# "22/tcp ALLOW IN Anywhere"
to = parts[0] if parts else ""
action = " ".join(parts[1:3]) if len(parts) > 2 else (parts[1] if len(parts) > 1 else "")
from_ = " ".join(parts[3:]) if len(parts) > 3 else ""
rules.append(Rule(num, to, action, from_, line))
return rules
async def ensure_ssh_allowed(c: SSHClient, ssh_port: int) -> None:
"""Защита от отстрела себя: разрешить текущий SSH-порт."""
rules = await list_rules(c)
for rule in rules:
if str(ssh_port) in rule.to and "ALLOW" in rule.action.upper():
return
await c.sudo(f"ufw allow {ssh_port}/tcp", timeout=30)
async def enable(c: SSHClient, ssh_port: int) -> str:
await ensure_ssh_allowed(c, ssh_port)
r = await c.sudo("yes | ufw enable", timeout=30)
return r.stdout + r.stderr
async def disable(c: SSHClient) -> str:
r = await c.sudo("ufw disable", timeout=30)
return r.stdout + r.stderr
async def add_rule(c: SSHClient, port: str, action: str = "allow",
proto: str = "tcp", from_addr: str = "") -> str:
assert action in ("allow", "deny", "limit", "reject")
src = f"from {from_addr} " if from_addr else ""
r = await c.sudo(f"ufw {action} {src}to any port {port} proto {proto}", timeout=30)
return r.stdout + r.stderr
async def delete_rule(c: SSHClient, number: int) -> str:
r = await c.sudo(f"yes | ufw delete {number}", timeout=30)
return r.stdout + r.stderr
async def listening_ports(c: SSHClient) -> List[str]:
r = await c.sudo("ss -tlnp | awk 'NR>1{print $4, $6}'", timeout=30)
return [l for l in r.stdout.splitlines() if l.strip()]
PRESETS = {
"Только SSH": [("{ssh_port}", "allow", "tcp")],
"Веб-сервер": [("{ssh_port}", "allow", "tcp"), ("80", "allow", "tcp"), ("443", "allow", "tcp")],
"Docker-хост": [("{ssh_port}", "allow", "tcp"), ("2375", "deny", "tcp"), ("2376", "deny", "tcp")],
}
async def apply_preset(c: SSHClient, preset_name: str, ssh_port: int) -> str:
log = []
for port, action, proto in PRESETS[preset_name]:
p = port.format(ssh_port=ssh_port)
log.append(await add_rule(c, p, action, proto))
return "\n".join(log)