198 lines
8.8 KiB
Python
198 lines
8.8 KiB
Python
"""
|
||
Сервис низкоуровневого ARP-сканирования локальной сети.
|
||
|
||
Назначение:
|
||
|
||
- Обнаружение активных узлов в пределах сетей хоста путём широковещательных ARP-запросов от имени выбранного интерфейса.
|
||
- Помощь сервисам более высокого уровня (например, IPService) при проверке занятости адресов перед применением конфигураций.
|
||
|
||
Особенности реализации:
|
||
|
||
- Определяет подключённые сети интерфейса через `netifaces` и строит CIDR для сканирования.
|
||
- Формирует ARP-пакеты с помощью `scapy` (Ether/ARP) и отправляет их с таймаутом.
|
||
- Добавляет локальный адрес интерфейса в результирующий список клиентов.
|
||
|
||
Требования/ограничения:
|
||
|
||
- Для отправки ARP-кадров могут потребоваться повышенные привилегии или capability на сырой сокет.
|
||
"""
|
||
import os
|
||
import logging
|
||
import netifaces
|
||
from netaddr import IPNetwork
|
||
from utils.i18n import _, _load_translations
|
||
from babel.support import Translations
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
try:
|
||
from scapy.layers.l2 import ARP, Ether
|
||
from scapy.sendrecv import srp
|
||
except Exception as exc: # pragma: no cover - fallback for doc builds
|
||
if os.getenv("SPHINX_BUILD"):
|
||
ARP = Ether = None # type: ignore
|
||
|
||
def srp(*args, **kwargs):
|
||
raise RuntimeError("scapy is unavailable during documentation build") from exc
|
||
else:
|
||
raise
|
||
|
||
class NetworkScanner:
|
||
"""Инициализация сетевого сканера.
|
||
|
||
Предоставляет методы для сканирования сети с использованием ARP-запросов и
|
||
вывода информации о клиентах.
|
||
"""
|
||
|
||
def __init__(self, interface=None, translations: Translations = None):
|
||
"""Инициализация параметров сетевого сканера.
|
||
|
||
Args:
|
||
interface (str, optional): Название сетевого интерфейса. По умолчанию
|
||
используется интерфейс по умолчанию из netifaces.gateways().
|
||
translations: Объект переводов для текущего языка.
|
||
|
||
Инициализирует интерфейс, информацию об IP-адресах и локальный MAC-адрес.
|
||
"""
|
||
self.translations = translations or _load_translations("ru")
|
||
self.interface = None
|
||
self.ip_info = []
|
||
self.networks = []
|
||
self.local_mac = None
|
||
|
||
try:
|
||
candidate_interface = interface
|
||
if not candidate_interface:
|
||
gateways = netifaces.gateways()
|
||
default_gateway = gateways.get("default", {}).get(netifaces.AF_INET)
|
||
if default_gateway:
|
||
candidate_interface = default_gateway[1]
|
||
else:
|
||
for iface in netifaces.interfaces():
|
||
if iface == "lo":
|
||
continue
|
||
addrs = netifaces.ifaddresses(iface)
|
||
if netifaces.AF_INET in addrs and addrs[netifaces.AF_INET]:
|
||
candidate_interface = iface
|
||
break
|
||
|
||
if candidate_interface:
|
||
self.interface = candidate_interface
|
||
addresses = netifaces.ifaddresses(self.interface)
|
||
self.ip_info = addresses.get(netifaces.AF_INET, [])
|
||
self.networks = [
|
||
IPNetwork(
|
||
f"{ip['addr']}/{ip['netmask']}"
|
||
) for ip in self.ip_info if ip.get("addr") and ip.get("netmask")
|
||
]
|
||
link_info = addresses.get(netifaces.AF_LINK, [])
|
||
if link_info:
|
||
self.local_mac = link_info[0].get("addr")
|
||
else:
|
||
logger.warning(
|
||
"NetworkScanner: IPv4 интерфейс не найден, сканирование будет недоступно"
|
||
)
|
||
except Exception as exc:
|
||
if os.getenv("SPHINX_BUILD"):
|
||
self.interface = interface or "eth0"
|
||
self.ip_info = []
|
||
self.networks = []
|
||
self.local_mac = "00:00:00:00:00:00"
|
||
logger.warning(
|
||
"NetworkScanner falling back to mocked interface during documentation build: %s",
|
||
exc,
|
||
)
|
||
else:
|
||
logger.error("NetworkScanner инициализация завершилась ошибкой: %s", exc)
|
||
raise
|
||
|
||
def _create_arp_packet(self, target_ip):
|
||
"""Создаёт ARP-пакет для отправки на указанный IP-адрес.
|
||
|
||
Args:
|
||
target_ip (str): Целевой IP-адрес или подсеть для сканирования.
|
||
|
||
Returns:
|
||
scapy.packet.Packet: Сформированный ARP-пакет с Ethernet-заголовком.
|
||
"""
|
||
arp = ARP(pdst=target_ip)
|
||
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
|
||
return ether / arp
|
||
|
||
def scan_network(self, attempts=3, timeout=2):
|
||
"""Сканирует сеть для обнаружения активных клиентов.
|
||
|
||
Args:
|
||
attempts (int, optional): Количество попыток отправки ARP-запросов.
|
||
По умолчанию 3.
|
||
timeout (int, optional): Таймаут ожидания ответа в секундах.
|
||
По умолчанию 2.
|
||
|
||
Returns:
|
||
List[dict]: Список словарей с ключами "ip" и "mac", представляющих
|
||
обнаруженных клиентов, включая локальные IP-адреса.
|
||
|
||
Note:
|
||
Учитывает локальные IP-адреса интерфейса и удаляет дубликаты.
|
||
"""
|
||
if not self.interface or not self.networks or ARP is None or Ether is None:
|
||
fallback_clients = []
|
||
if self.ip_info:
|
||
for ip_info in self.ip_info:
|
||
fallback_clients.append({
|
||
"ip": ip_info.get("addr"),
|
||
"mac": self.local_mac,
|
||
})
|
||
return [client for client in fallback_clients if client["ip"]]
|
||
|
||
all_clients = []
|
||
|
||
for network in self.networks:
|
||
target_ip = f"{network.network}/{network.prefixlen}"
|
||
packet = self._create_arp_packet(target_ip)
|
||
clients = []
|
||
|
||
for _ in range(attempts):
|
||
result = srp(packet,
|
||
timeout=timeout,
|
||
iface=self.interface,
|
||
verbose=0)[0]
|
||
for _, received in result:
|
||
client = {"ip": received.psrc, "mac": received.hwsrc}
|
||
if client not in clients:
|
||
clients.append(client)
|
||
|
||
all_clients.extend(clients)
|
||
|
||
for ip_info in self.ip_info:
|
||
if not ip_info.get("addr"):
|
||
continue
|
||
local_client = {
|
||
"ip": ip_info["addr"],
|
||
"mac": self.local_mac,
|
||
}
|
||
if local_client not in all_clients:
|
||
all_clients.append(local_client)
|
||
|
||
unique_clients = {tuple(client.items()) for client in all_clients}
|
||
return [dict(client) for client in unique_clients]
|
||
|
||
def print_clients(self, clients):
|
||
"""Выводит список адресов клиентов в формате таблицы.
|
||
|
||
Args:
|
||
clients (List[dict]): Список словарей с ключами "ip" и "mac",
|
||
представляющих клиентов.
|
||
|
||
Выводит IP-адреса и MAC-адреса в отсортированном по IP порядке.
|
||
"""
|
||
print(self.translations.gettext("Доступные устройства в сети:"))
|
||
print(self.translations.gettext("IP") + " " * 18 + self.translations.gettext("MAC"))
|
||
for client in sorted(clients, key=lambda x: x["ip"]):
|
||
print(f"{client['ip']:<16} {client['mac']}")
|
||
|
||
# Оставил нативный запуск сервиса для тестов
|
||
if __name__ == "__main__":
|
||
scanner = NetworkScanner()
|
||
clients = scanner.scan_network()
|
||
scanner.print_clients(clients)
|