gonets/SRV/models/ufw.py

263 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from typing import List, Optional, Annotated, Union
from enum import Enum
from pydantic import BaseModel, Field, validator
import ipaddress
class Protocol(str, Enum):
"""
Enum для сетевых протоколов UFW.
"""
TCP = "tcp"
UDP = "udp"
BOTH = "both"
@property
def protocols(self) -> List[str]:
"""Возвращает список протоколов для использования в команде UFW."""
return ["tcp", "udp"] if self == Protocol.BOTH else [self.value]
class Action(str, Enum):
"""
Enum для действий UFW.
Attributes:
ALLOW: Разрешить трафик
DENY: Запретить трафик без уведомления
REJECT: Запретить трафик с уведомлением отправителю
"""
ALLOW = "allow"
DENY = "deny"
REJECT = "reject"
class Direction(str, Enum):
"""
Enum для направления трафика UFW.
Attributes:
IN: Входящий трафик
OUT: Исходящий трафик
"""
IN = "in"
OUT = "out"
class UFWRule(BaseModel):
"""Модель правила UFW"""
action: Action
direction: Direction
from_ip: str
to_ip: str
port: Union[int, str]
protocol: Protocol
comment: Optional[str] = None
@validator("port")
def validate_port(cls, v):
"""
Проверяет корректность порта или диапазона портов.
Args:
v: Порт (число) или диапазон портов (строка вида "3000:3999")
Returns:
Union[int, str]: Валидированное значение порта
Raises:
ValueError: Если порт или диапазон портов некорректен
"""
if isinstance(v, int):
if not (1 <= v <= 65535):
raise ValueError(f"Порт должен быть в диапазоне 1-65535, получено: {v}")
return v
elif isinstance(v, str):
try:
start, end = map(int, v.split(':'))
if not (1 <= start <= end <= 65535):
raise ValueError
return v
except (ValueError, TypeError):
raise ValueError(f"Некорректный диапазон портов: {v}. Формат: start:end, где 1 <= start <= end <= 65535")
else:
raise ValueError(f"Порт должен быть числом или строкой с диапазоном, получено: {type(v)}")
@validator("from_ip", "to_ip")
def validate_ip(cls, v):
"""Проверяет корректность IP адреса"""
try:
# Пробуем создать объект IP-сети, что автоматически проверит корректность адреса
ipaddress.ip_network(v, strict=False)
return v
except ValueError:
raise ValueError(f"Некорректный IP адрес или подсеть: {v}")
@validator("to_ip")
def validate_ip_version_compatibility(cls, v, values):
"""Проверяет совместимость версий IP адресов"""
if "from_ip" not in values:
return v
# Определяем версии IP
from_ip = ipaddress.ip_network(values["from_ip"], strict=False)
to_ip = ipaddress.ip_network(v, strict=False)
if isinstance(from_ip, ipaddress.IPv6Network) != isinstance(to_ip, ipaddress.IPv6Network):
raise ValueError("Несовместимые версии IP: нельзя смешивать IPv4 и IPv6 в одном правиле")
return v
@validator("action", pre=True)
def validate_action(cls, v):
"""Приводит значение action к нижнему регистру перед валидацией"""
if isinstance(v, str):
return v.lower()
return v
@validator("protocol", pre=True)
def validate_protocol(cls, v):
"""Приводит значение protocol к нижнему регистру перед валидацией"""
if isinstance(v, str):
return v.lower()
return v
def to_ufw_command(self) -> str:
"""Преобразует правило в команду UFW."""
cmd_parts = ["ufw"]
# Добавляем действие
cmd_parts.append(self.action.value)
# Добавляем направление
cmd_parts.append(self.direction.value)
# Добавляем from с IP
cmd_parts.extend(["from", self.from_ip])
# Добавляем to с IP
cmd_parts.extend(["to", self.to_ip])
# Добавляем порт и протокол
port_str = str(self.port) # Работает как с числами, так и с диапазонами
cmd_parts.extend(["port", port_str])
# Добавляем протокол
proto = self.protocol.value
if proto == "both":
cmd_parts.append("proto tcp/udp")
else:
cmd_parts.extend(["proto", proto])
# Добавляем комментарий если есть
if self.comment:
cmd_parts.extend(["comment", f"'{self.comment}'"])
return " ".join(cmd_parts)
@classmethod
def from_ufw_output(cls, line: str) -> "UFWRule":
"""
Создает экземпляр UFWRule из строки вывода команды 'ufw status numbered'.
Args:
line: Строка вывода UFW (например, "[1] 80/tcp ALLOW IN 192.168.1.0/24 # Web Server")
Returns:
UFWRule: Новый экземпляр правила
"""
import re
# Регулярное выражение для разбора строки правила UFW
pattern = r"""^\s*
\[\s*(?P<number>\d+)\s*\]\s*
(?P<port>\d+(?::\d+)?)/(?P<protocol>tcp(?:/udp)?|udp)\s+
(?P<action>ALLOW|DENY|REJECT)\s+(?P<direction>IN|OUT)\s+
(?P<from_ip>[\w\d\./]+|\bAnywhere\b)
(?:\s+\(v6\))?
(?:\s+\((?:out|in)\))?
\s*
(?:\#\s*(?P<comment>.*?))?
\s*$
"""
match = re.match(pattern, line, re.VERBOSE | re.IGNORECASE)
if not match:
raise ValueError(f"Невозможно разобрать строку правила UFW: {line}")
groups = match.groupdict()
# Преобразуем протокол в enum
proto_str = groups['protocol'].lower()
protocol = Protocol.BOTH if proto_str == "tcp/udp" else \
Protocol.UDP if proto_str == "udp" else Protocol.TCP
# Преобразуем действие в enum (игнорируем регистр)
action_str = groups['action'].lower()
action = Action.ALLOW if action_str == "allow" else \
Action.DENY if action_str == "deny" else Action.REJECT
# Преобразуем направление в enum
direction_str = groups['direction'].lower()
direction = Direction.IN if direction_str == "in" else Direction.OUT
# Обрабатываем IP адреса
from_ip = groups['from_ip'].strip()
if from_ip.lower() == "anywhere":
from_ip = "0.0.0.0/0"
# Для исходящих правил меняем местами from и to
to_ip = "0.0.0.0/0" # По умолчанию anywhere
# Обрабатываем порт
port = groups['port']
if ':' in port:
# Оставляем как строку для диапазона
port = port.strip()
else:
# Преобразуем в число для одиночного порта
port = int(port)
# Обрабатываем комментарий
comment = groups.get('comment', '').strip() if groups.get('comment') else None
return cls(
action=action,
direction=direction,
from_ip=from_ip,
to_ip=to_ip,
port=port,
protocol=protocol,
comment=comment
)
class UFWRulesResponse(BaseModel):
"""
Модель для представления списка правил UFW.
Attributes:
enabled: Включен ли UFW
rules: Список правил UFW
total_rules: Общее количество правил
"""
enabled: bool = Field(..., description="Включен ли UFW")
rules: List[UFWRule] = Field(default_factory=list, description="Список правил UFW")
@property
def total_rules(self) -> int:
"""Возвращает общее количество правил."""
return len(self.rules)
class UFWStatus(BaseModel):
"""
Модель для представления статуса UFW.
Attributes:
enabled: Включен ли UFW
message: Сообщение о статусе
last_checked: Время последней проверки статуса
"""
enabled: bool = Field(..., description="Включен ли UFW")
message: str = Field(..., description="Сообщение о статусе")