2025-06-09 10:17:12 +05:00
|
|
|
|
import ipaddress
|
|
|
|
|
|
import subprocess
|
|
|
|
|
|
from typing import List, Union
|
|
|
|
|
|
|
|
|
|
|
|
from models.ufw import Direction, Protocol, UFWRule, UFWRulesResponse, UFWStatus
|
|
|
|
|
|
|
|
|
|
|
|
DEBUG = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UFWService:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Сервис для взаимодействия с утилитой UFW (Uncomplicated Firewall) в Linux.
|
|
|
|
|
|
Позволяет получить список правил UFW и управлять ими.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def _run_command(self, command: str) -> str:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Выполняет команду в системе и возвращает ее вывод.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
command (str): Команда для выполнения в системе.
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
str: Вывод команды.
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if DEBUG:
|
|
|
|
|
|
print(f"Running command: {command}")
|
|
|
|
|
|
|
|
|
|
|
|
# Используем shell=True для корректной обработки аргументов с пробелами
|
|
|
|
|
|
result = subprocess.run(
|
|
|
|
|
|
f"sudo {command}",
|
|
|
|
|
|
shell=True,
|
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
|
stderr=subprocess.PIPE,
|
|
|
|
|
|
text=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if DEBUG:
|
|
|
|
|
|
print(f"Command output: {result.stdout}")
|
|
|
|
|
|
print(f"Command error: {result.stderr}")
|
|
|
|
|
|
|
|
|
|
|
|
if result.returncode != 0:
|
2025-06-12 16:47:27 +05:00
|
|
|
|
raise subprocess.CalledProcessError(result.returncode, command,
|
|
|
|
|
|
result.stdout,
|
|
|
|
|
|
result.stderr)
|
2025-06-09 10:17:12 +05:00
|
|
|
|
|
|
|
|
|
|
return result.stdout
|
|
|
|
|
|
|
|
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
|
|
|
|
if DEBUG:
|
|
|
|
|
|
print(f"Command failed with return code {e.returncode}")
|
|
|
|
|
|
print(f"Error output: {e.stderr}")
|
|
|
|
|
|
raise Exception(f"Command failed: {e.stderr}")
|
|
|
|
|
|
|
|
|
|
|
|
def _get_port_range_args(self, port: Union[int, str]) -> List[str]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Преобразует порт или диапазон портов в аргументы команды UFW.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
port: Порт (число) или диапазон портов (строка вида "3000:3999")
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
List[str]: Список аргументов для UFW
|
|
|
|
|
|
"""
|
|
|
|
|
|
if isinstance(port, int):
|
|
|
|
|
|
return [str(port)]
|
|
|
|
|
|
elif isinstance(port, str) and ":" in port:
|
|
|
|
|
|
# Диапазон портов уже в нужном формате
|
|
|
|
|
|
return [port]
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise ValueError(f"Некорректный формат порта: {port}")
|
|
|
|
|
|
|
|
|
|
|
|
def get_ufw_rules(self) -> UFWRulesResponse:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Получает список правил UFW.
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
UFWRulesResponse: Объект, содержащий статус UFW и список правил
|
|
|
|
|
|
"""
|
|
|
|
|
|
# Check UFW status
|
|
|
|
|
|
status_result = self._run_command("ufw status numbered")
|
|
|
|
|
|
lines = status_result.splitlines()
|
|
|
|
|
|
|
|
|
|
|
|
if not lines:
|
|
|
|
|
|
return UFWRulesResponse(enabled=False, rules=[])
|
|
|
|
|
|
|
|
|
|
|
|
first_line = lines[0]
|
|
|
|
|
|
is_enabled = "Status: active" in first_line
|
|
|
|
|
|
|
|
|
|
|
|
if not is_enabled:
|
|
|
|
|
|
return UFWRulesResponse(enabled=False, rules=[])
|
|
|
|
|
|
|
|
|
|
|
|
rules = []
|
|
|
|
|
|
# Skip header lines
|
|
|
|
|
|
for line in lines[4:]: # Skip Status and header lines
|
|
|
|
|
|
if not line.strip() or not line.startswith("["):
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
rule = UFWRule.from_ufw_output(line)
|
|
|
|
|
|
rules.append(rule)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
if DEBUG:
|
|
|
|
|
|
print(f"Ошибка парсинга правила: {line}, ошибка: {str(e)}")
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
return UFWRulesResponse(enabled=is_enabled, rules=rules)
|
|
|
|
|
|
|
|
|
|
|
|
def get_ufw_status(self) -> UFWStatus:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Проверяет статус UFW.
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
UFWStatus: Объект с информацией о статусе UFW
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
status_result = self._run_command("ufw status")
|
2025-06-12 16:47:27 +05:00
|
|
|
|
first_line = (status_result.splitlines()[0]
|
|
|
|
|
|
if status_result.splitlines() else "")
|
2025-06-09 10:17:12 +05:00
|
|
|
|
is_enabled = "Status: active" in first_line
|
|
|
|
|
|
|
|
|
|
|
|
message = "UFW активен" if is_enabled else "UFW неактивен"
|
|
|
|
|
|
|
|
|
|
|
|
if DEBUG:
|
|
|
|
|
|
print(f"Результат статуса: {status_result}")
|
|
|
|
|
|
print(f"Включен: {is_enabled}")
|
|
|
|
|
|
|
|
|
|
|
|
return UFWStatus(enabled=is_enabled, message=message)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
if DEBUG:
|
|
|
|
|
|
print(f"Ошибка при проверке статуса UFW: {e}")
|
|
|
|
|
|
return UFWStatus(
|
|
|
|
|
|
enabled=False,
|
|
|
|
|
|
message=f"Ошибка при проверке статуса: {str(e)}",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def enable_ufw(self) -> bool:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Включает UFW.
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
bool: True если успешно включен, False в противном случае
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
self._run_command("ufw enable")
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
if DEBUG:
|
|
|
|
|
|
print(f"Ошибка при включении UFW: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def disable_ufw(self) -> bool:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Выключает UFW.
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
bool: True если успешно выключен, False в противном случае
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
self._run_command("ufw disable")
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
if DEBUG:
|
|
|
|
|
|
print(f"Ошибка при выключении UFW: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
2025-06-12 16:47:27 +05:00
|
|
|
|
def add_rules_base(self,
|
|
|
|
|
|
rules: List[UFWRule],
|
|
|
|
|
|
direction: str = "") -> bool:
|
2025-06-09 10:17:12 +05:00
|
|
|
|
"""
|
|
|
|
|
|
Базовый метод для добавления правил UFW.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
rules: Список правил для добавления
|
|
|
|
|
|
direction: Направление трафика ("in" или "out")
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
bool: True если все правила были успешно добавлены, False в противном случае
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
for rule in rules:
|
|
|
|
|
|
# Устанавливаем направление правила
|
|
|
|
|
|
rule.direction = Direction.IN if direction == "in" else Direction.OUT
|
|
|
|
|
|
|
|
|
|
|
|
# Формируем базовую команду
|
|
|
|
|
|
command = f"ufw {rule.action.value}"
|
|
|
|
|
|
|
|
|
|
|
|
# Добавляем направление
|
|
|
|
|
|
command += f" {rule.direction.value}"
|
|
|
|
|
|
|
|
|
|
|
|
# Добавляем from IP если он указан
|
|
|
|
|
|
if rule.from_ip and rule.from_ip != "any":
|
|
|
|
|
|
command += f" from {rule.from_ip}"
|
|
|
|
|
|
else:
|
|
|
|
|
|
command += " from any"
|
|
|
|
|
|
|
|
|
|
|
|
# Добавляем to IP если он указан
|
|
|
|
|
|
if rule.to_ip and rule.to_ip != "any":
|
|
|
|
|
|
command += f" to {rule.to_ip}"
|
|
|
|
|
|
else:
|
|
|
|
|
|
command += " to any"
|
|
|
|
|
|
|
|
|
|
|
|
# Добавляем порт если он указан
|
|
|
|
|
|
if rule.port:
|
|
|
|
|
|
command += f" port {rule.port}"
|
|
|
|
|
|
|
|
|
|
|
|
# Добавляем протокол
|
|
|
|
|
|
if rule.protocol != Protocol.BOTH:
|
|
|
|
|
|
command += f" proto {rule.protocol.value}"
|
|
|
|
|
|
|
|
|
|
|
|
# Добавляем комментарий если он есть
|
|
|
|
|
|
if rule.comment:
|
|
|
|
|
|
command += f" comment '{rule.comment}'"
|
|
|
|
|
|
|
|
|
|
|
|
self._run_command(command)
|
|
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
if DEBUG:
|
|
|
|
|
|
print(f"Error adding UFW rules: {str(e)}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def add_ufw_in_rules(self, rules: List[UFWRule]) -> bool:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Добавляет входящие (in) правила UFW.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
rules (List[UFWRule]): Список правил для добавления
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
bool: True если все правила были успешно добавлены, False в противном случае
|
|
|
|
|
|
"""
|
|
|
|
|
|
return self.add_rules_base(rules, "in")
|
|
|
|
|
|
|
|
|
|
|
|
def add_ufw_out_rules(self, rules: List[UFWRule]) -> bool:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Добавляет исходящие (out) правила UFW.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
rules (List[UFWRule]): Список правил для добавления
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
bool: True если все правила были успешно добавлены, False в противном случае
|
|
|
|
|
|
"""
|
|
|
|
|
|
return self.add_rules_base(rules, "out")
|
|
|
|
|
|
|
|
|
|
|
|
def add_rules(self, rules: List[UFWRule]) -> dict:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Добавляет правила UFW.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
rules: Список правил для добавления
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
dict: Результат добавления правил
|
|
|
|
|
|
"""
|
|
|
|
|
|
results = []
|
|
|
|
|
|
|
|
|
|
|
|
for rule in rules:
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Определяем версию IP
|
|
|
|
|
|
from_ip = ipaddress.ip_network(rule.from_ip, strict=False)
|
|
|
|
|
|
is_ipv6 = isinstance(from_ip, ipaddress.IPv6Network)
|
|
|
|
|
|
|
|
|
|
|
|
# Получаем список протоколов
|
|
|
|
|
|
protocols = rule.protocol.protocols
|
|
|
|
|
|
print(f"Обработка правила для протоколов: {protocols}")
|
|
|
|
|
|
|
|
|
|
|
|
for proto in protocols:
|
|
|
|
|
|
# Формируем базовую команду
|
|
|
|
|
|
command = f"ufw {rule.action.value}"
|
|
|
|
|
|
|
|
|
|
|
|
# Добавляем IP адреса
|
|
|
|
|
|
command += f" from {rule.from_ip}"
|
|
|
|
|
|
if rule.to_ip:
|
|
|
|
|
|
command += f" to {rule.to_ip}"
|
|
|
|
|
|
|
|
|
|
|
|
# Добавляем порт и протокол
|
|
|
|
|
|
port_args = self._get_port_range_args(rule.port)
|
|
|
|
|
|
command += f" port {' '.join(port_args)} proto {proto}"
|
|
|
|
|
|
|
|
|
|
|
|
# Добавляем флаг IPv6 если нужно
|
|
|
|
|
|
if is_ipv6:
|
|
|
|
|
|
command += " v6"
|
|
|
|
|
|
|
|
|
|
|
|
# Добавляем комментарий если есть
|
|
|
|
|
|
if rule.comment:
|
|
|
|
|
|
command += f" comment '{rule.comment}'"
|
|
|
|
|
|
|
|
|
|
|
|
print(f"Подготовлена команда: {command}")
|
|
|
|
|
|
|
|
|
|
|
|
# Выполняем команду
|
|
|
|
|
|
try:
|
|
|
|
|
|
output = self._run_command(command)
|
|
|
|
|
|
results.append({"success": True, "message": output})
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
error_msg = f"Ошибка выполнения команды: {str(e)}"
|
|
|
|
|
|
print(error_msg)
|
2025-06-12 16:47:27 +05:00
|
|
|
|
results.append({
|
|
|
|
|
|
"success": False,
|
|
|
|
|
|
"message": error_msg
|
|
|
|
|
|
})
|
2025-06-09 10:17:12 +05:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
error_msg = f"Ошибка при добавлении правила: {str(e)}"
|
|
|
|
|
|
print(error_msg)
|
|
|
|
|
|
results.append({"success": False, "message": error_msg})
|
|
|
|
|
|
|
|
|
|
|
|
# Выводим результаты
|
|
|
|
|
|
print("\nРезультаты добавления правил:")
|
|
|
|
|
|
for result in results:
|
|
|
|
|
|
status = "Успешно" if result["success"] else "Ошибка"
|
|
|
|
|
|
print(f" - {status}: {result['message']}")
|
|
|
|
|
|
|
|
|
|
|
|
return {"success": True, "results": results}
|
|
|
|
|
|
|
|
|
|
|
|
def delete_rules(self, rule_numbers: List[int]) -> dict:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Удаляет правила UFW по их номерам.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
rule_numbers (List[int]): Список номеров правил для удаления
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
dict: Результат операции удаления
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Проверяем существование правил
|
|
|
|
|
|
status_result = self._run_command("ufw status numbered")
|
|
|
|
|
|
lines = status_result.splitlines()
|
|
|
|
|
|
|
|
|
|
|
|
# Пропускаем заголовок (первые 4 строки)
|
|
|
|
|
|
rules = []
|
|
|
|
|
|
for line in lines[4:]: # Skip header lines
|
|
|
|
|
|
if not line.strip() or not line.startswith("["):
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Извлекаем номер правила из строки [N]
|
2025-06-12 16:47:27 +05:00
|
|
|
|
rule_num = int(line[1:line.index("]")])
|
2025-06-09 10:17:12 +05:00
|
|
|
|
rules.append(rule_num)
|
|
|
|
|
|
except (ValueError, IndexError):
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
if DEBUG:
|
|
|
|
|
|
print(f"Найдены правила: {rules}")
|
|
|
|
|
|
|
|
|
|
|
|
results = []
|
|
|
|
|
|
# Сортируем номера в обратном порядке, чтобы удаление происходило с конца
|
|
|
|
|
|
# Это предотвратит смещение номеров при удалении
|
|
|
|
|
|
for rule_number in sorted(rule_numbers, reverse=True):
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Проверяем существует ли правило с таким номером
|
|
|
|
|
|
if rule_number not in rules:
|
2025-06-12 16:47:27 +05:00
|
|
|
|
results.append({
|
|
|
|
|
|
"number":
|
|
|
|
|
|
rule_number,
|
|
|
|
|
|
"success":
|
|
|
|
|
|
False,
|
|
|
|
|
|
"error":
|
|
|
|
|
|
f"Правило с номером {rule_number} не найдено",
|
|
|
|
|
|
})
|
2025-06-09 10:17:12 +05:00
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# Удаляем правило
|
|
|
|
|
|
self._run_command(f"ufw --force delete {rule_number}")
|
|
|
|
|
|
|
2025-06-12 16:47:27 +05:00
|
|
|
|
results.append({
|
|
|
|
|
|
"number":
|
|
|
|
|
|
rule_number,
|
|
|
|
|
|
"success":
|
|
|
|
|
|
True,
|
|
|
|
|
|
"message":
|
|
|
|
|
|
f"Правило {rule_number} успешно удалено",
|
|
|
|
|
|
})
|
2025-06-09 10:17:12 +05:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
if DEBUG:
|
2025-06-12 16:47:27 +05:00
|
|
|
|
print(
|
|
|
|
|
|
f"Ошибка при удалении правила {rule_number}: {e}")
|
|
|
|
|
|
results.append({
|
|
|
|
|
|
"number": rule_number,
|
|
|
|
|
|
"success": False,
|
|
|
|
|
|
"error": str(e)
|
|
|
|
|
|
})
|
2025-06-09 10:17:12 +05:00
|
|
|
|
|
|
|
|
|
|
# Подсчитываем статистику
|
|
|
|
|
|
total = len(rule_numbers)
|
|
|
|
|
|
successful = len([r for r in results if r["success"]])
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"success": True,
|
|
|
|
|
|
"total": total,
|
|
|
|
|
|
"successful": successful,
|
|
|
|
|
|
"results": results,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
if DEBUG:
|
|
|
|
|
|
print(f"Ошибка при удалении правил: {e}")
|
|
|
|
|
|
return {"success": False, "error": str(e), "results": []}
|