gonets/SRV/services/subnet_service.py

282 lines
No EOL
12 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 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(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
class NetworkScanner:
"""Класс для сканирования сети с помощью ARP-запросов."""
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
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:
"""Класс для сканирования портов на устройстве."""
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:
logger.debug(f"Ошибка проверки порта {port} для IP {ip}: {str(e)}")
return None
try:
max_workers = min(len(port_config.get_ports()), 15) # Снижено с 100
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:
logger.warning(f"Ошибка при сканировании портов для IP {ip}: {str(e)}")
return []
class DeviceInfoCollector:
"""Класс для сбора информации об устройстве."""
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(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:
"""Класс для хранения и управления картой устройств в JSON."""
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 {}
def _clean_old_devices(self, devices: Dict[str, Device]) -> Dict[str, Device]:
now = datetime.now(ZoneInfo("UTC"))
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:
logger.info(f"Удалено устройство {mac} (offline > 7 дней)")
except ValueError:
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)
logger.info(f"Сохранено {len(devices)} устройств в {self.storage_file}")
except Exception as e:
logger.error(f"Ошибка при сохранении устройств: {str(e)}")
class SubnetService:
"""Сервис для сканирования подсети и управления картой устройств."""
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()
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"
def scan_subnet(self, subnet: str, ports: List[int] = None) -> SubnetScanResult:
start_time = time.time()
devices = []
now = datetime.now(ZoneInfo("UTC")).isoformat()
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 = 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=now
)
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=now
)
with ThreadPoolExecutor(max_workers=10) as executor:
device_results = executor.map(process_device, discovered_devices)
devices.extend([d for d in device_results if d is not None])
# Добавляет устройства со старой карты, в нынешнюю при отсутствии со статусом offline
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(ZoneInfo("UTC")).isoformat()
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=now
)
logger.warning(f"Локальное устройство не найдено в подсети {subnet}")
return None
except Exception as e:
logger.warning(f"Не удалось получить локальное устройство: {str(e)}")
return None