gonets/SRV/services/subnet_service.py

405 lines
17 KiB
Python
Raw Normal View History

from scapy.config import conf
from mac_vendor_lookup import MacLookup
from typing import List, Optional, Tuple, Dict
import socket
import logging
import netifaces
import ipaddress
from concurrent.futures import ThreadPoolExecutor
import time
from functools import lru_cache
import json
from pathlib import Path
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from models.subnet import SubnetScanResult, Device
from config.ports import port_config
from scapy.layers.inet import IP, TCP
from scapy.layers.l2 import ARP, Ether
from scapy.sendrecv import srp, sr1
logging.basicConfig(
2025-06-12 16:47:27 +05:00
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
class NetworkScanner:
2025-06-12 16:47:27 +05:00
def __init__(self, interface: str):
self.interface = interface
conf.iface = interface
def scan(self, subnet: str) -> List[Tuple[str, str]]:
try:
arp = ARP(pdst=subnet)
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
packet = ether / arp
2025-06-12 16:47:27 +05:00
result = srp(packet,
timeout=2,
verbose=False,
iface=self.interface)[0]
return [(received.psrc, received.hwsrc)
for sent, received in result]
except Exception as e:
logger.error(f"Ошибка ARP-сканирования подсети {subnet}: {str(e)}")
return []
class PortScanner:
2025-06-12 16:47:27 +05:00
def __init__(self, interface: str):
self.interface = interface
def scan(self, ip: str) -> List[str]:
open_ports = []
def check_port(port: int) -> Optional[str]:
try:
pkt = sr1(
IP(dst=ip) / TCP(dport=port, flags="S"),
timeout=2,
verbose=False,
iface=self.interface,
)
if pkt and pkt.haslayer(TCP) and pkt[TCP].flags == 0x12:
return port_config.get_port_name(port)
return None
except Exception as e:
2025-06-12 16:47:27 +05:00
logger.debug(
f"Ошибка проверки порта {port} для IP {ip}: {str(e)}")
return None
try:
max_workers = min(len(port_config.get_ports()), 15)
with ThreadPoolExecutor(max_workers=max_workers) as executor:
results = executor.map(check_port, port_config.get_ports())
open_ports = [port for port in results if port is not None]
logger.debug(f"Открытые порты для IP {ip}: {open_ports}")
return open_ports
except Exception as e:
2025-06-12 16:47:27 +05:00
logger.warning(
f"Ошибка при сканировании портов для IP {ip}: {str(e)}")
return []
class DeviceInfoCollector:
2025-06-12 16:47:27 +05:00
def __init__(self):
self.mac_lookup = MacLookup()
@lru_cache(maxsize=1000)
def get_vendor(self, mac: str) -> str:
try:
return self.mac_lookup.lookup(mac)
except Exception as e:
logger.warning(
2025-06-12 16:47:27 +05:00
f"Не удалось определить производителя для MAC {mac}: {str(e)}")
return "Unknown"
def get_hostname(self, ip: str) -> Optional[str]:
try:
return socket.gethostbyaddr(ip)[0]
except:
return None
class DeviceStorage:
2025-06-12 16:47:27 +05:00
def __init__(self, storage_path: str = "storage/devices/devices.json"):
self.storage_file = Path(storage_path)
self.storage_file.parent.mkdir(parents=True, exist_ok=True)
self.offline_timeout = timedelta(days=7)
def load_devices(self) -> Dict[str, Device]:
try:
if self.storage_file.exists():
with open(self.storage_file, "r", encoding="utf-8") as f:
data = json.load(f)
devices = {}
for device_data in data.get("devices", []):
device = Device(**device_data)
if device.mac:
devices[device.mac] = device
return self._clean_old_devices(devices)
return {}
except Exception as e:
logger.error(f"Ошибка при загрузке устройств: {str(e)}")
return {}
2025-06-12 16:47:27 +05:00
def _clean_old_devices(self, devices: Dict[str,
Device]) -> Dict[str, Device]:
now = datetime.now()
filtered = {}
for mac, device in devices.items():
if device.status == "offline" and device.last_seen:
try:
last_seen = datetime.fromisoformat(device.last_seen)
if now - last_seen <= self.offline_timeout:
filtered[mac] = device
else:
2025-06-12 16:47:27 +05:00
logger.info(
f"Удалено устройство {mac} (offline > 7 дней)")
except ValueError:
2025-06-12 16:47:27 +05:00
logger.warning(
f"Неверный формат last_seen для устройства {mac}")
else:
filtered[mac] = device
return filtered
def save_devices(self, devices: List[Device]) -> None:
try:
data = SubnetScanResult(devices=devices).model_dump()
with open(self.storage_file, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
2025-06-12 16:47:27 +05:00
logger.info(
f"Сохранено {len(devices)} устройств в {self.storage_file}")
except Exception as e:
logger.error(f"Ошибка при сохранении устройств: {str(e)}")
class SubnetService:
2025-06-12 16:47:27 +05:00
def __init__(self):
self.interface = self._get_default_interface()
self.network_scanner = NetworkScanner(self.interface)
self.port_scanner = PortScanner(self.interface)
self.device_info = DeviceInfoCollector()
self.storage = DeviceStorage()
def _get_default_interface(self) -> str:
try:
gateways = netifaces.gateways()
2025-06-12 16:47:27 +05:00
default_gateway = gateways.get("default",
{}).get(netifaces.AF_INET)
if default_gateway:
return default_gateway[1]
interfaces = netifaces.interfaces()
for iface in interfaces:
if iface != "lo":
return iface
return "eth0"
except Exception as e:
logger.warning(f"Не удалось определить интерфейс: {str(e)}")
return "eth0"
2025-06-12 16:47:27 +05:00
def scan_subnet(self,
subnet: str,
ports: List[int] = None) -> SubnetScanResult:
start_time = time.time()
devices = []
now = datetime.now().astimezone()
2025-06-17 12:21:53 +05:00
formatted_now = now.strftime("%d-%m-%Y, %H:%M")
try:
saved_devices = self.storage.load_devices()
local_device = self._get_local_device(subnet)
if local_device:
local_device.status = "online"
local_device.last_seen = formatted_now
devices.append(local_device)
if local_device.mac:
saved_devices[local_device.mac] = local_device
discovered_devices = self.network_scanner.scan(subnet)
def process_device(ip_mac: Tuple[str, str]) -> Optional[Device]:
ip, mac = ip_mac
if local_device and ip == local_device.ip:
return None
existing_device = saved_devices.get(mac)
if existing_device:
return Device(
ip=ip,
mac=mac,
hostname=self.device_info.get_hostname(ip)
or existing_device.hostname,
vendor=self.device_info.get_vendor(mac),
open_ports=self.port_scanner.scan(ip)
or existing_device.open_ports,
status="online",
last_seen=formatted_now,
note=existing_device.note,
)
vendor = self.device_info.get_vendor(mac)
hostname = self.device_info.get_hostname(ip)
open_ports = self.port_scanner.scan(ip)
return Device(
ip=ip,
mac=mac,
hostname=hostname,
vendor=vendor,
open_ports=open_ports,
status="online",
last_seen=formatted_now,
note=None,
)
with ThreadPoolExecutor(max_workers=10) as executor:
2025-06-12 16:47:27 +05:00
device_results = executor.map(process_device,
discovered_devices)
devices.extend([d for d in device_results if d is not None])
active_macs = {d.mac for d in devices if d.mac}
for mac, device in saved_devices.items():
if mac not in active_macs:
device.status = "offline"
devices.append(device)
devices.sort(key=lambda x: int(ipaddress.IPv4Address(x.ip)))
for device in devices:
if device.mac:
saved_devices[device.mac] = device
self.storage.save_devices(list(saved_devices.values()))
logger.info(
f"Сканирование подсети {subnet} завершено за {time.time() - start_time:.2f} секунд, "
f"найдено {len(devices)} устройств (активных: {len(active_macs)})"
)
return SubnetScanResult(devices=devices)
except Exception as e:
logger.error(f"Ошибка при сканировании подсети {subnet}: {str(e)}")
return SubnetScanResult(devices=devices)
def _get_local_device(self, subnet: str) -> Optional[Device]:
now = datetime.now().astimezone()
2025-06-17 12:21:53 +05:00
formatted_now = now.strftime("%d-%m-%Y, %H:%M")
try:
subnet_network = ipaddress.ip_network(subnet, strict=False)
interfaces = netifaces.interfaces()
for iface in interfaces:
if iface == "lo":
continue
addrs = netifaces.ifaddresses(iface)
if netifaces.AF_INET in addrs and netifaces.AF_LINK in addrs:
ip = addrs[netifaces.AF_INET][0]["addr"]
if ipaddress.ip_address(ip) in subnet_network:
mac = addrs[netifaces.AF_LINK][0]["addr"]
vendor = self.device_info.get_vendor(mac)
hostname = self.device_info.get_hostname(ip)
open_ports = self.port_scanner.scan(ip)
logger.debug(
f"Локальное устройство: IP={ip}, MAC={mac}, Ports={open_ports}"
)
return Device(
ip=ip,
mac=mac,
hostname=hostname,
vendor=vendor,
open_ports=open_ports,
status="online",
last_seen=formatted_now,
note=None,
)
2025-06-12 16:47:27 +05:00
logger.warning(
f"Локальное устройство не найдено в подсети {subnet}")
return None
except Exception as e:
2025-06-12 16:47:27 +05:00
logger.warning(
f"Не удалось получить локальное устройство: {str(e)}")
return None
def add_note(self, mac: str, note: str) -> Tuple[bool, Optional[str]]:
"""Добавляет заметку к устройству по MAC-адресу.
Args:
mac (str): MAC-адрес устройства.
note (str): Текст заметки.
Returns:
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
- Успех: (True, None).
- Ошибка: (False, сообщение об ошибке).
Raises:
Exception: Если произошла ошибка при сохранении.
"""
try:
saved_devices = self.storage.load_devices()
if mac not in saved_devices:
logger.warning(f"Устройство с MAC {mac} не найдено")
return False, f"Устройство с MAC {mac} не найдено"
if saved_devices[mac].note is not None:
2025-06-12 16:47:27 +05:00
logger.error(
f"Заметка для устройства с MAC {mac} уже существует")
return False, f"Заметка для устройства с MAC {mac} уже существует"
saved_devices[mac].note = note
self.storage.save_devices(list(saved_devices.values()))
2025-06-12 16:47:27 +05:00
logger.info(
f"Добавлена заметка '{note}' для устройства с MAC {mac}")
return True, None
except Exception as e:
2025-06-12 16:47:27 +05:00
logger.error(
f"Ошибка при добавлении заметки для MAC {mac}: {str(e)}")
return False, f"Внутренняя ошибка сервера: {str(e)}"
def update_note(self, mac: str, note: str) -> Tuple[bool, Optional[str]]:
"""Обновляет заметку устройства по MAC-адресу.
Args:
mac (str): MAC-адрес устройства.
note (str): Новый текст заметки.
Returns:
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
- Успех: (True, None).
- Ошибка: (False, сообщение об ошибке).
Raises:
Exception: Если произошла ошибка при сохранении.
"""
try:
saved_devices = self.storage.load_devices()
if mac not in saved_devices:
logger.warning(f"Устройство с MAC {mac} не найдено")
return False, f"Устройство с MAC {mac} не найдено"
if saved_devices[mac].note is None:
2025-06-12 16:47:27 +05:00
logger.error(
f"Заметка для устройства с MAC {mac} не существует")
return False, f"Заметка для устройства с MAC {mac} не существует"
saved_devices[mac].note = note
self.storage.save_devices(list(saved_devices.values()))
2025-06-12 16:47:27 +05:00
logger.info(
f"Обновлена заметка '{note}' для устройства с MAC {mac}")
return True, None
except Exception as e:
2025-06-12 16:47:27 +05:00
logger.error(
f"Ошибка при обновлении заметки для MAC {mac}: {str(e)}")
return False, f"Внутренняя ошибка сервера: {str(e)}"
def delete_note(self, mac: str) -> Tuple[bool, Optional[str]]:
"""Удаляет заметку устройства по MAC-адресу.
Args:
mac (str): MAC-адрес устройства.
Returns:
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
- Успех: (True, None).
- Ошибка: (False, сообщение об ошибке).
Raises:
Exception: Если произошла ошибка при сохранении.
"""
try:
saved_devices = self.storage.load_devices()
if mac not in saved_devices:
logger.warning(f"Устройство с MAC {mac} не найдено")
return False, f"Устройство с MAC {mac} не найдено"
if saved_devices[mac].note is None:
2025-06-12 16:47:27 +05:00
logger.error(
f"Заметка для устройства с MAC {mac} не существует")
return False, f"Заметка для устройства с MAC {mac} не существует"
saved_devices[mac].note = None
self.storage.save_devices(list(saved_devices.values()))
logger.info(f"Удалена заметка для устройства с MAC {mac}")
return True, None
except Exception as e:
2025-06-12 16:47:27 +05:00
logger.error(
f"Ошибка при удалении заметки для MAC {mac}: {str(e)}")
return False, f"Внутренняя ошибка сервера: {str(e)}"