"""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)