158 lines
5.5 KiB
Python
158 lines
5.5 KiB
Python
"""
|
|
Widget de detección de VLANs en un trunk (Scapy).
|
|
Funciona en Windows (con Npcap) y en Linux.
|
|
"""
|
|
from PyQt6.QtWidgets import (
|
|
QWidget,
|
|
QVBoxLayout,
|
|
QHBoxLayout,
|
|
QLabel,
|
|
QComboBox,
|
|
QPushButton,
|
|
QGroupBox,
|
|
QTableWidget,
|
|
QTableWidgetItem,
|
|
QHeaderView,
|
|
QMessageBox,
|
|
)
|
|
from PyQt6.QtCore import Qt
|
|
|
|
from src.core.vlan_scanner import VLANScanner
|
|
|
|
|
|
class VLANDetectionWidget(QWidget):
|
|
"""Solo detección pasiva de VLANs (Scapy). Sin bridge ni tcpdump."""
|
|
|
|
def __init__(self, network_manager):
|
|
super().__init__()
|
|
self.nm = network_manager
|
|
self.vlan_scanner = VLANScanner()
|
|
self.scan_thread = None
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
group = QGroupBox("Detección de VLANs en trunk")
|
|
group_layout = QVBoxLayout(group)
|
|
|
|
controls = QHBoxLayout()
|
|
controls.addWidget(QLabel("Interfaz:"))
|
|
self.iface_combo = QComboBox()
|
|
controls.addWidget(self.iface_combo)
|
|
self.scan_btn = QPushButton("Iniciar escaneo (10 s)")
|
|
self.scan_btn.clicked.connect(self.start_scan)
|
|
controls.addWidget(self.scan_btn)
|
|
group_layout.addLayout(controls)
|
|
|
|
self.scan_table = QTableWidget()
|
|
self.scan_table.setColumnCount(2)
|
|
self.scan_table.setHorizontalHeaderLabels(["VLAN ID", "Paquetes detectados"])
|
|
self.scan_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
|
self.scan_table.setMinimumHeight(200)
|
|
group_layout.addWidget(self.scan_table)
|
|
|
|
self.port_type_label = QLabel("Tipo de puerto: —")
|
|
self.port_type_label.setStyleSheet("font-weight: bold; color: #aaaaaa;")
|
|
group_layout.addWidget(self.port_type_label)
|
|
|
|
layout.addWidget(group)
|
|
|
|
info = QLabel(
|
|
"Conecta esta interfaz a un puerto trunk y pulsa Iniciar escaneo. "
|
|
"En Windows requiere Npcap y ejecutar como Administrador."
|
|
)
|
|
info.setWordWrap(True)
|
|
info.setStyleSheet("color: #888; font-size: 11px;")
|
|
layout.addWidget(info)
|
|
|
|
self.refresh_interfaces()
|
|
|
|
def refresh_interfaces(self):
|
|
self.iface_combo.clear()
|
|
for iface in self.nm.list_interfaces():
|
|
self.iface_combo.addItem(iface["name"])
|
|
|
|
def start_scan(self):
|
|
if self.scan_thread and self.scan_thread.isRunning():
|
|
self.scan_thread.stop()
|
|
self.scan_btn.setEnabled(False)
|
|
self.scan_btn.setText("Deteniendo...")
|
|
return
|
|
|
|
iface = self.iface_combo.currentText()
|
|
if not iface:
|
|
QMessageBox.warning(self, "Error", "Selecciona una interfaz.")
|
|
return
|
|
|
|
self.scan_table.setRowCount(0)
|
|
self.port_type_label.setText("Tipo de puerto: Escaneando...")
|
|
self.port_type_label.setStyleSheet("font-weight: bold; color: #e0e000;")
|
|
self.scan_btn.setText("Detener escaneo")
|
|
self.scan_btn.setStyleSheet("background-color: #800000; color: white;")
|
|
|
|
self.scan_thread = self.vlan_scanner.scan_scapy(iface, timeout=10)
|
|
self.scan_thread.vlan_detected.connect(self.on_vlan_detected)
|
|
self.scan_thread.untagged_detected.connect(self.on_untagged_detected)
|
|
self.scan_thread.scan_finished.connect(self.on_scan_finished)
|
|
self.scan_thread.error_occurred.connect(self.on_scan_error)
|
|
|
|
def on_untagged_detected(self, count):
|
|
self._update_row("Untagged (Native)", count)
|
|
self._update_port_analysis()
|
|
|
|
def on_vlan_detected(self, vlan_id, count):
|
|
self._update_row(str(vlan_id), count)
|
|
self._update_port_analysis()
|
|
|
|
def _update_row(self, label, count):
|
|
for row in range(self.scan_table.rowCount()):
|
|
if self.scan_table.item(row, 0).text() == label:
|
|
self.scan_table.setItem(row, 1, QTableWidgetItem(str(count)))
|
|
return
|
|
row = self.scan_table.rowCount()
|
|
self.scan_table.insertRow(row)
|
|
self.scan_table.setItem(row, 0, QTableWidgetItem(label))
|
|
self.scan_table.setItem(row, 1, QTableWidgetItem(str(count)))
|
|
|
|
def _update_port_analysis(self):
|
|
has_untagged = False
|
|
vlan_count = 0
|
|
for row in range(self.scan_table.rowCount()):
|
|
label = self.scan_table.item(row, 0).text()
|
|
if label == "Untagged (Native)":
|
|
has_untagged = True
|
|
else:
|
|
vlan_count += 1
|
|
if vlan_count > 1:
|
|
msg = f"Trunk ({vlan_count} VLANs)"
|
|
color = "#00e000"
|
|
elif vlan_count == 1 and has_untagged:
|
|
msg = "Híbrido (tagged + untagged)"
|
|
color = "#e0e000"
|
|
elif vlan_count == 1:
|
|
msg = "Trunk (1 VLAN)"
|
|
color = "#00e000"
|
|
elif has_untagged:
|
|
msg = "Access (solo untagged)"
|
|
color = "#00e0e0"
|
|
else:
|
|
msg = "— / Sin tráfico"
|
|
color = "#aaaaaa"
|
|
self.port_type_label.setText(f"Tipo de puerto: {msg}")
|
|
self.port_type_label.setStyleSheet(f"font-weight: bold; color: {color};")
|
|
|
|
def on_scan_finished(self):
|
|
self.scan_btn.setEnabled(True)
|
|
self.scan_btn.setText("Iniciar escaneo (10 s)")
|
|
self.scan_btn.setStyleSheet("")
|
|
|
|
def on_scan_error(self, msg):
|
|
self.scan_btn.setEnabled(True)
|
|
self.scan_btn.setText("Iniciar escaneo (10 s)")
|
|
self.scan_btn.setStyleSheet("")
|
|
QMessageBox.critical(self, "Error", msg)
|
|
|
|
def closeEvent(self, event):
|
|
if self.scan_thread and self.scan_thread.isRunning():
|
|
self.scan_thread.stop()
|
|
event.accept()
|